Skip to content

Commit b7aa4fa

Browse files
committed
feat(a11y): improved LabelRequiredForA11y type definition
This also updated the `*Props` types for the following components so the `*Props` are always the "strict" `LabelRequiredForA11y` version: - `Dialog` - `FixedDialog` - `Menu` - `Sheet` If you want to use the props without the strictness, you can use the `Base*` prefixed version. So `BaseDialogProps` instead of `DialogProps`.
1 parent 9c6834a commit b7aa4fa

7 files changed

Lines changed: 34 additions & 27 deletions

File tree

packages/dialog/src/Dialog.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323

2424
import useNestedDialogFixes from "./useNestedDialogFixes";
2525

26-
export interface DialogProps
26+
export interface BaseDialogProps
2727
extends OverridableCSSTransitionProps,
2828
RenderConditionalPortalProps,
2929
FocusContainerOptionsProps,
@@ -188,7 +188,7 @@ export interface DialogProps
188188
component?: "div" | "nav";
189189
}
190190

191-
type StrictProps = LabelRequiredForA11y<DialogProps>;
191+
export type DialogProps = LabelRequiredForA11y<BaseDialogProps>;
192192

193193
// used to disable the overlay click-to-close functionality when the `modal` prop is enabled.
194194
const noop = (): void => {};
@@ -206,7 +206,7 @@ const DEFAULT_DIALOG_TIMEOUT = {
206206
exit: 150,
207207
};
208208

209-
const Dialog = forwardRef<HTMLDivElement, StrictProps>(function Dialog(
209+
const Dialog = forwardRef<HTMLDivElement, DialogProps>(function Dialog(
210210
{
211211
component = "div",
212212
tabIndex = -1,

packages/dialog/src/FixedDialog.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
OptionalFixedPositionOptions,
88
useFixedPositioning,
99
} from "@react-md/transition";
10-
import { LabelRequiredForA11y, TOP_INNER_RIGHT_ANCHOR } from "@react-md/utils";
10+
import { TOP_INNER_RIGHT_ANCHOR, LabelRequiredForA11y } from "@react-md/utils";
1111

12-
import Dialog, { DialogProps } from "./Dialog";
12+
import Dialog, { BaseDialogProps } from "./Dialog";
1313

14-
export interface FixedDialogProps
15-
extends Omit<DialogProps, "type">,
14+
export interface BaseFixedDialogProps
15+
extends Omit<BaseDialogProps, "type">,
1616
Pick<OptionalFixedPositionOptions, "anchor"> {
1717
/**
1818
* The element the dialog should be fixed to. This can either be:
@@ -35,7 +35,7 @@ export interface FixedDialogProps
3535
getOptions?: GetFixedPositionOptions;
3636
}
3737

38-
type StrictProps = LabelRequiredForA11y<FixedDialogProps>;
38+
export type FixedDialogProps = LabelRequiredForA11y<BaseFixedDialogProps>;
3939

4040
const DEFAULT_CLASSNAMES: CSSTransitionClassNames = {
4141
appear: "rmd-dialog--fixed-enter",
@@ -51,7 +51,7 @@ const DEFAULT_CLASSNAMES: CSSTransitionClassNames = {
5151
* be fix itself to another element. Another term for this component might be a
5252
* "Pop out Dialog".
5353
*/
54-
const FixedDialog = forwardRef<HTMLDivElement, StrictProps>(
54+
const FixedDialog = forwardRef<HTMLDivElement, FixedDialogProps>(
5555
function FixedDialog(
5656
{
5757
fixedTo,

packages/documentation/src/components/Demos/Sheet/MobileActionSheet.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
MenuRenderer,
1313
} from "@react-md/menu";
1414
import { Sheet, SheetProps } from "@react-md/sheet";
15-
import { LabelRequiredForA11y, useAppSize } from "@react-md/utils";
15+
import { useAppSize } from "@react-md/utils";
1616

1717
import styles from "./MobileActionSheet.module.scss";
1818

@@ -23,10 +23,7 @@ const items = [
2323
{ leftAddon: <DeleteSVGIcon />, children: "Delete collection" },
2424
];
2525

26-
const MenuSheet: FC<LabelRequiredForA11y<SheetProps>> = ({
27-
children,
28-
...props
29-
}) => {
26+
const MenuSheet: FC<SheetProps> = ({ children, ...props }) => {
3027
const { onRequestClose } = props;
3128
const handleClick = useCallback(
3229
(event: React.MouseEvent) => {

packages/documentation/src/constants/sandboxes/Sheet-MobileActionSheet.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"isBinary": false
5050
},
5151
"src/Demo.tsx": {
52-
"content": "import React, { FC, useCallback } from \"react\";\nimport {\n ArrowDropDownSVGIcon,\n DeleteSVGIcon,\n EditSVGIcon,\n LinkSVGIcon,\n ShareSVGIcon,\n} from \"@react-md/material-icons\";\nimport {\n defaultMenuRenderer,\n DropdownMenu,\n MenuRenderer,\n} from \"@react-md/menu\";\nimport { Sheet, SheetProps } from \"@react-md/sheet\";\nimport { LabelRequiredForA11y, useAppSize } from \"@react-md/utils\";\n\nimport styles from \"./MobileActionSheet.module.scss\";\n\nconst items = [\n { leftAddon: <ShareSVGIcon />, children: \"Share\" },\n { leftAddon: <LinkSVGIcon />, children: \"Get link\" },\n { leftAddon: <EditSVGIcon />, children: \"Edit name\" },\n { leftAddon: <DeleteSVGIcon />, children: \"Delete collection\" },\n];\n\nconst MenuSheet: FC<LabelRequiredForA11y<SheetProps>> = ({\n children,\n ...props\n}) => {\n const { onRequestClose } = props;\n const handleClick = useCallback(\n (event: React.MouseEvent) => {\n if (event.target !== event.currentTarget) {\n onRequestClose();\n }\n },\n [onRequestClose]\n );\n\n return (\n <Sheet\n {...props}\n className={styles.sheet}\n onRequestClose={onRequestClose}\n role=\"menu\"\n disableFocusOnMount\n position=\"bottom\"\n onClick={handleClick}\n >\n {children}\n </Sheet>\n );\n};\n\nconst renderSheet: MenuRenderer = ({\n // these props are only required for the `Menu` component, but not within the sheet\n // so we can just extract them and not pass them down\n horizontal: _horizontal,\n controlId: _controlId,\n anchor: _anchor,\n positionOptions: _positionOptions,\n closeOnResize: _closeOnResize,\n closeOnScroll: _closeOnScroll,\n ...props\n}) => <MenuSheet {...props} />;\n\nconst Demo: FC = () => {\n const { isTablet, isLandscape, isDesktop, isLargeDesktop } = useAppSize();\n const sheet = !isDesktop && !isLargeDesktop && !(isTablet && isLandscape);\n return (\n <DropdownMenu\n id=\"dropdown-menu-1\"\n items={items}\n dropdownIcon={<ArrowDropDownSVGIcon />}\n menuRenderer={sheet ? renderSheet : defaultMenuRenderer}\n >\n Dropdown\n </DropdownMenu>\n );\n};\n\nexport default Demo;\n",
52+
"content": "import React, { FC, useCallback } from \"react\";\nimport {\n ArrowDropDownSVGIcon,\n DeleteSVGIcon,\n EditSVGIcon,\n LinkSVGIcon,\n ShareSVGIcon,\n} from \"@react-md/material-icons\";\nimport {\n defaultMenuRenderer,\n DropdownMenu,\n MenuRenderer,\n} from \"@react-md/menu\";\nimport { Sheet, SheetProps } from \"@react-md/sheet\";\nimport { useAppSize } from \"@react-md/utils\";\n\nimport styles from \"./MobileActionSheet.module.scss\";\n\nconst items = [\n { leftAddon: <ShareSVGIcon />, children: \"Share\" },\n { leftAddon: <LinkSVGIcon />, children: \"Get link\" },\n { leftAddon: <EditSVGIcon />, children: \"Edit name\" },\n { leftAddon: <DeleteSVGIcon />, children: \"Delete collection\" },\n];\n\nconst MenuSheet: FC<SheetProps> = ({ children, ...props }) => {\n const { onRequestClose } = props;\n const handleClick = useCallback(\n (event: React.MouseEvent) => {\n if (event.target !== event.currentTarget) {\n onRequestClose();\n }\n },\n [onRequestClose]\n );\n\n return (\n <Sheet\n {...props}\n className={styles.sheet}\n onRequestClose={onRequestClose}\n role=\"menu\"\n disableFocusOnMount\n position=\"bottom\"\n onClick={handleClick}\n >\n {children}\n </Sheet>\n );\n};\n\nconst renderSheet: MenuRenderer = ({\n // these props are only required for the `Menu` component, but not within the sheet\n // so we can just extract them and not pass them down\n horizontal: _horizontal,\n controlId: _controlId,\n anchor: _anchor,\n positionOptions: _positionOptions,\n closeOnResize: _closeOnResize,\n closeOnScroll: _closeOnScroll,\n ...props\n}) => <MenuSheet {...props} />;\n\nconst Demo: FC = () => {\n const { isTablet, isLandscape, isDesktop, isLargeDesktop } = useAppSize();\n const sheet = !isDesktop && !isLargeDesktop && !(isTablet && isLandscape);\n return (\n <DropdownMenu\n id=\"dropdown-menu-1\"\n items={items}\n dropdownIcon={<ArrowDropDownSVGIcon />}\n menuRenderer={sheet ? renderSheet : defaultMenuRenderer}\n >\n Dropdown\n </DropdownMenu>\n );\n};\n\nexport default Demo;\n",
5353
"isBinary": false
5454
},
5555
"src/MobileActionSheet.module.scss": {

packages/menu/src/Menu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export type MenuPositionOptions = Omit<
2525
"container" | "element" | "anchor"
2626
>;
2727

28-
export interface MenuProps
28+
export interface BaseMenuProps
2929
extends HTMLAttributes<HTMLDivElement>,
3030
OverridableCSSTransitionProps,
3131
RenderConditionalPortalProps {
@@ -136,7 +136,7 @@ export interface MenuProps
136136
disableControlClickOkay?: boolean;
137137
}
138138

139-
type StrictProps = LabelRequiredForA11y<MenuProps>;
139+
export type MenuProps = LabelRequiredForA11y<BaseMenuProps>;
140140

141141
const block = bem("rmd-menu");
142142

@@ -145,7 +145,7 @@ const block = bem("rmd-menu");
145145
* out based on the `visible` prop as well as handle keyboard focus, closing
146146
* when needed, etc.
147147
*/
148-
const Menu = forwardRef<HTMLDivElement, StrictProps>(function Menu(
148+
const Menu = forwardRef<HTMLDivElement, MenuProps>(function Menu(
149149
{
150150
role = "menu",
151151
tabIndex = -1,

packages/sheet/src/Sheet.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import React, { forwardRef, useRef, useState, useCallback } from "react";
22
import cn from "classnames";
3-
import { Dialog, DialogProps } from "@react-md/dialog";
3+
import { Dialog, BaseDialogProps } from "@react-md/dialog";
44
import { bem, LabelRequiredForA11y } from "@react-md/utils";
55
import { DEFAULT_SHEET_TIMEOUT, DEFAULT_SHEET_CLASSNAMES } from "./constants";
66

77
type AllowedDialogProps = Omit<
8-
DialogProps,
8+
BaseDialogProps,
99
"role" | "type" | "modal" | "forceContainer"
1010
>;
1111

1212
export type SheetPosition = "top" | "right" | "bottom" | "left";
1313
export type SheetHorizontalSize = "none" | "media" | "touch" | "static";
1414
export type SheetVerticalSize = "none" | "touch" | "recommended";
1515

16-
export interface SheetProps extends AllowedDialogProps {
16+
export interface BaseSheetProps extends AllowedDialogProps {
1717
/**
1818
* The role that the sheet should be rendered as. You'll normally want to keep
1919
* this as the default of `"dialog"` unless you are implementing a mobile
@@ -60,7 +60,7 @@ export interface SheetProps extends AllowedDialogProps {
6060
verticalSize?: SheetVerticalSize;
6161
}
6262

63-
type StrictProps = LabelRequiredForA11y<SheetProps>;
63+
export type SheetProps = LabelRequiredForA11y<BaseSheetProps>;
6464

6565
const block = bem("rmd-sheet");
6666

@@ -69,7 +69,7 @@ const block = bem("rmd-sheet");
6969
* to the edges of the viewport instead of centered or full page. This component
7070
* is great for rendering a navigation tree or menus on mobile devices.
7171
*/
72-
const Sheet = forwardRef<HTMLDivElement, StrictProps>(function Sheet(
72+
const Sheet = forwardRef<HTMLDivElement, SheetProps>(function Sheet(
7373
{
7474
className,
7575
children,

packages/utils/src/types.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,27 @@ export type ClassNameCloneableChild<T = {}> = ReactElement<
2525
* This type allows you to require at least one of the provided keys. This is
2626
* super helpful for things like `aria-label` or `aria-labelledby` when it's
2727
* required for a11y.
28+
*
29+
* @see https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist/49725198#49725198
2830
*/
2931
export type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
3032
T,
3133
Exclude<keyof T, Keys>
3234
> &
33-
{ [K in Keys]-?: Required<Pick<T, K>> }[Keys];
35+
{
36+
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
37+
}[Keys];
3438

35-
interface LabelA11y {
39+
export interface LabelA11y {
3640
"aria-label"?: string;
3741
"aria-labelledby"?: string;
3842
}
3943

40-
export type LabelRequiredForA11y<T extends LabelA11y> = T &
41-
RequireAtLeastOne<T, "aria-label" | "aria-labelledby">;
44+
/**
45+
* A small accessibility helper to ensure that either `aria-label` or
46+
* `aria-labelledby` have been provided to a component.
47+
*/
48+
export type LabelRequiredForA11y<Props extends LabelA11y> = RequireAtLeastOne<
49+
Props,
50+
keyof LabelA11y
51+
>;

0 commit comments

Comments
 (0)