Skip to content

Commit

Permalink
feat(components): add Perceivable to support aria-disabled for RAC (
Browse files Browse the repository at this point in the history
#1488)

* feat(components): add `Perceivable` to support `aria-disabled` for RAC

* fix: scope with data attribute
  • Loading branch information
Niznikr authored Nov 20, 2024
1 parent 07a1d78 commit 5ab1acf
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-bags-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@launchpad-ui/components": patch
---

Add `Perceivable` to support `aria-disabled` for RAC
38 changes: 38 additions & 0 deletions packages/components/__tests__/Perceivable.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, it, vi } from 'vitest';

import { render, screen, userEvent, waitFor } from '../../../test/utils';
import { Button, Perceivable, Tooltip, TooltipTrigger } from '../src';

describe('Perceivable', () => {
it('sets aria-disabled', async () => {
render(
<Perceivable>
<TooltipTrigger>
<Button>Button</Button>
<Tooltip>Message</Tooltip>
</TooltipTrigger>
</Perceivable>,
);

await waitFor(() => {
expect(screen.getByRole('button', { hidden: true })).toHaveAttribute('aria-disabled', 'true');
});
});

it('prevents events', async () => {
const spy = vi.fn();
const user = userEvent.setup();

render(
<Perceivable>
<TooltipTrigger>
<Button onPress={spy}>Button</Button>
<Tooltip>Message</Tooltip>
</TooltipTrigger>
</Perceivable>,
);

await user.click(screen.getByRole('button', { hidden: true }));
expect(spy).toHaveBeenCalledTimes(0);
});
});
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@internationalized/date": "3.5.6",
"@launchpad-ui/icons": "workspace:~",
"@launchpad-ui/tokens": "workspace:~",
"@react-aria/focus": "3.18.4",
"@react-aria/toast": "3.0.0-beta.17",
"@react-aria/utils": "3.25.3",
"@react-stately/toast": "3.0.0-beta.6",
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from 'react-aria-components';

import { input } from './Input';
import { PerceivableContext } from './Perceivable';
import { ProgressBar } from './ProgressBar';
import styles from './styles/Button.module.css';

Expand Down Expand Up @@ -47,9 +48,12 @@ const _Button = (
) => {
const selectContext = useSlottedContext(SelectContext);
const state = useContext(SelectStateContext);
const ctx = useContext(PerceivableContext);

return (
<AriaButton
{...props}
{...ctx}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
state
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import type { ButtonVariants } from './Button';

import { Icon } from '@launchpad-ui/icons';
import { cva, cx } from 'class-variance-authority';
import { forwardRef } from 'react';
import { forwardRef, useContext } from 'react';
import { Button as AriaButton, composeRenderProps } from 'react-aria-components';

import { button } from './Button';
import { PerceivableContext } from './Perceivable';
import styles from './styles/IconButton.module.css';

const iconButton = cva(styles.base, {
Expand Down Expand Up @@ -43,9 +44,11 @@ const _IconButton = (
{ size = 'medium', variant = 'default', icon, ...props }: IconButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
) => {
const ctx = useContext(PerceivableContext);
return (
<AriaButton
{...props}
{...ctx}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
cx(button({ ...renderProps, size, variant, className }), iconButton({ size })),
Expand Down
39 changes: 39 additions & 0 deletions packages/components/src/Perceivable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { KeyboardEvents, PressEvents } from '@react-types/shared';
import type { ReactNode } from 'react';

import { FocusableProvider } from '@react-aria/focus';
import { createContext } from 'react';
import { Provider } from 'react-aria-components';

interface InteractionProps extends KeyboardEvents, PressEvents {}

interface PerceivableProps {
children: ReactNode;
}

const PerceivableContext = createContext<InteractionProps>({});

const Perceivable = ({ children }: PerceivableProps) => {
const props = {
onPress: undefined,
onPressStart: undefined,
onPressEnd: undefined,
onPressChange: undefined,
onPressUp: undefined,
onKeyDown: undefined,
onKeyUp: undefined,
onClick: undefined,
href: undefined,
};

return (
<Provider values={[[PerceivableContext, { ...props }]]}>
<FocusableProvider aria-disabled="true" data-lp="">
{children}
</FocusableProvider>
</Provider>
);
};

export { Perceivable, PerceivableContext };
export type { PerceivableProps };
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export { Menu, MenuItem, MenuTrigger, SubmenuTrigger } from './Menu';
export { Meter } from './Meter';
export { Modal, ModalOverlay } from './Modal';
export { NumberField } from './NumberField';
export { Perceivable } from './Perceivable';
export { OverlayArrow, Popover } from './Popover';
export { Pressable } from './Pressable';
export { ProgressBar } from './ProgressBar';
Expand Down
5 changes: 5 additions & 0 deletions packages/components/src/styles/base.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,8 @@
cursor: not-allowed;
}
}

:global([data-lp][aria-disabled='true']) {
opacity: 0.64;
cursor: default;
}
37 changes: 35 additions & 2 deletions packages/components/stories/composition.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Meta, StoryFn, StoryObj } from '@storybook/react';
import type { ComponentPropsWithoutRef, Fragment } from 'react';
import type { ComponentPropsWithoutRef } from 'react';

import { Icon } from '@launchpad-ui/icons';
import { vars } from '@launchpad-ui/vars';
import { expect, userEvent, within } from '@storybook/test';
import { useRef, useState } from 'react';
import { type Fragment, useRef, useState } from 'react';
import { VisuallyHidden } from 'react-aria';

import {
Expand All @@ -15,11 +15,13 @@ import {
Dialog,
DialogTrigger,
Group,
IconButton,
Input,
Label,
ListBox,
ListBoxItem,
type ListBoxItemProps,
Perceivable,
Popover,
RadioButton,
RadioGroup,
Expand Down Expand Up @@ -222,3 +224,34 @@ export const ListBoxTooltip: Story = {
},
name: 'ListBox Tooltip',
};

export const DisabledWithTooltip: Story = {
render: () => {
return (
<div
style={{
display: 'inline-flex',
flexDirection: 'column',
gap: vars.spacing[400],
}}
>
<Perceivable>
<TooltipTrigger>
<Button onPress={() => console.log('Pressed')}>Button</Button>
<Tooltip placement="right">Message</Tooltip>
</TooltipTrigger>
<TooltipTrigger>
<IconButton
icon="add"
size="small"
variant="minimal"
aria-label="create"
onPress={() => console.log('Pressed')}
/>
<Tooltip placement="right">Message</Tooltip>
</TooltipTrigger>
</Perceivable>
</div>
);
},
};
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5ab1acf

Please sign in to comment.