Skip to content

Commit acecfe2

Browse files
feat: Tag remove-button focus ring (#8194)
* feat: Tag remove button focus ring * always add focus ring to clear button * fix styles * fix safari * add focus ring to react aria * fix animation and emphasized --------- Co-authored-by: Danni <darobins@adobe.com>
1 parent 8f83612 commit acecfe2

File tree

9 files changed

+123
-53
lines changed

9 files changed

+123
-53
lines changed

packages/@adobe/spectrum-css-temp/components/button/index.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,24 @@ a.spectrum-ActionButton {
372372
margin-block: 0;
373373
margin-inline: auto;
374374
}
375+
376+
&.spectrum-ClearButton--inset.spectrum-ClearButton--inset {
377+
transition: unset;
378+
box-sizing: border-box;
379+
&:after {
380+
transition: unset;
381+
left: 4px;
382+
right: 4px;
383+
bottom: 4px;
384+
top: 4px;
385+
}
386+
387+
&:focus-visible {
388+
&:after {
389+
box-shadow: inset 0 0 0 var(--spectrum-focus-ring-size) var(--spectrum-focus-ring-color);
390+
}
391+
}
392+
}
375393
}
376394

377395
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {

packages/@react-aria/tag/docs/useTagGroup.mdx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,14 @@ interface TagProps<T> extends AriaTagProps<T> {
134134
function Tag<T>(props: TagProps<T>) {
135135
let {item, state} = props;
136136
let ref = React.useRef(null);
137-
let {focusProps, isFocusVisible} = useFocusRing({within: true});
137+
let {focusProps, isFocusVisible} = useFocusRing({within: false});
138138
let {rowProps, gridCellProps, removeButtonProps, allowsRemoving} = useTag(props, state, ref);
139139

140140
return (
141141
<div ref={ref} {...rowProps} {...focusProps} data-focus-visible={isFocusVisible}>
142142
<div {...gridCellProps}>
143143
{item.rendered}
144-
{allowsRemoving && <Button {...removeButtonProps}></Button>}
144+
{allowsRemoving && <Button {...removeButtonProps}></Button>}
145145
</div>
146146
</div>
147147
);
@@ -172,13 +172,16 @@ function Tag<T>(props: TagProps<T>) {
172172
}
173173

174174
.tag-group [role="row"] {
175-
display: flex;
176-
align-items: center;
177175
border: 1px solid gray;
176+
forced-color-adjust: none;
178177
border-radius: 4px;
179-
padding: 2px 5px;
180-
cursor: default;
178+
padding: 2px 8px;
179+
font-size: 0.929rem;
181180
outline: none;
181+
cursor: default;
182+
display: flex;
183+
align-items: center;
184+
transition: border-color 200ms;
182185

183186
&[data-focus-visible=true] {
184187
outline: 2px solid slateblue;
@@ -197,13 +200,24 @@ function Tag<T>(props: TagProps<T>) {
197200
}
198201

199202
.tag-group [role="gridcell"] {
200-
margin: 0 5px;
203+
display: contents;
201204
}
202205

203206
.tag-group [role="row"] button {
204207
background: none;
205208
border: none;
206-
padding-right: 0;
209+
padding: 0;
210+
margin-left: 4px;
211+
outline: none;
212+
font-size: 0.95em;
213+
border-radius: 100%;
214+
aspect-ratio: 1/1;
215+
height: 100%;
216+
217+
&[data-focus-visible=true] {
218+
outline: 2px solid slateblue;
219+
outline-offset: -1px;
220+
}
207221
}
208222

209223
.tag-group .description {
@@ -227,11 +241,13 @@ The `Button` component is used in the above example to remove a tag. It is built
227241

228242
```tsx example export=true render=false
229243
import {useButton} from '@react-aria/button';
244+
import {mergeProps} from '@react-aria/utils';
230245

231246
function Button(props) {
232247
let ref = React.useRef(null);
233248
let {buttonProps} = useButton(props, ref);
234-
return <button {...buttonProps} ref={ref}>{props.children}</button>;
249+
let {focusProps, isFocusVisible} = useFocusRing({within: false});
250+
return <button {...mergeProps(buttonProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible}>{props.children}</button>;
235251
}
236252
```
237253

packages/@react-spectrum/button/src/ClearButton.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ interface ClearButtonProps<T extends ElementType = 'button'> extends ButtonProps
2525
focusClassName?: string,
2626
variant?: 'overBackground',
2727
excludeFromTabOrder?: boolean,
28-
preventFocus?: boolean
28+
preventFocus?: boolean,
29+
inset?: boolean
2930
}
3031

3132
export const ClearButton = React.forwardRef(function ClearButton(props: ClearButtonProps, ref: FocusableRef<HTMLButtonElement>) {
@@ -37,6 +38,7 @@ export const ClearButton = React.forwardRef(function ClearButton(props: ClearBut
3738
isDisabled,
3839
preventFocus,
3940
elementType = preventFocus ? 'div' : 'button' as ElementType,
41+
inset = false,
4042
...otherProps
4143
} = props;
4244
let domRef = useFocusableRef(ref);
@@ -66,7 +68,8 @@ export const ClearButton = React.forwardRef(function ClearButton(props: ClearBut
6668
[`spectrum-ClearButton--${variant}`]: variant,
6769
'is-disabled': isDisabled,
6870
'is-active': isPressed,
69-
'is-hovered': isHovered
71+
'is-hovered': isHovered,
72+
'spectrum-ClearButton--inset': inset
7073
},
7174
styleProps.className
7275
)

packages/@react-spectrum/s2/src/ClearButton.tsx

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,61 @@ import {
1818
import {controlSize} from './style-utils' with {type: 'macro'};
1919
import CrossIcon from '../ui-icons/Cross';
2020
import {FocusableRef} from '@react-types/shared';
21+
import {focusRing, style} from '../style' with {type: 'macro'};
2122
import {forwardRef} from 'react';
22-
import {style} from '../style' with {type: 'macro'};
23+
import {pressScale} from './pressScale';
2324
import {useFocusableRef} from '@react-spectrum/utils';
24-
2525
interface ClearButtonStyleProps {
2626
/**
2727
* The size of the ClearButton.
2828
*
2929
* @default 'M'
3030
*/
31-
size?: 'S' | 'M' | 'L' | 'XL'
31+
size?: 'S' | 'M' | 'L' | 'XL',
32+
/** Whether the ClearButton should be displayed with a static color. */
33+
isStaticColor?: boolean
3234
}
3335

3436
interface ClearButtonRenderProps extends ButtonRenderProps, ClearButtonStyleProps {}
3537
interface ClearButtonProps extends ButtonProps, ClearButtonStyleProps {}
3638

39+
const focusRingStyles = focusRing();
40+
41+
const visibleClearButton = style<ClearButtonRenderProps>({
42+
...focusRingStyles,
43+
display: 'flex',
44+
alignItems: 'center',
45+
justifyContent: 'center',
46+
height: 'full',
47+
width: controlSize(),
48+
flexShrink: 0,
49+
borderRadius: 'full',
50+
borderStyle: 'none',
51+
backgroundColor: 'transparent',
52+
boxSizing: 'border-box',
53+
padding: 0,
54+
outlineOffset: -4,
55+
outlineColor: {
56+
default: focusRingStyles.outlineColor,
57+
isStaticColor: 'white'
58+
},
59+
color: 'inherit',
60+
'--iconPrimary': {
61+
type: 'fill',
62+
value: 'currentColor'
63+
}
64+
});
65+
3766
export const ClearButton = forwardRef(function ClearButton(props: ClearButtonProps, ref: FocusableRef<HTMLButtonElement>) {
67+
let {size = 'M', isStaticColor = false, ...rest} = props;
3868
let domRef = useFocusableRef(ref);
39-
4069
return (
4170
<Button
42-
{...props}
71+
{...rest}
4372
ref={domRef}
44-
className={renderProps => style<ClearButtonRenderProps>({
45-
display: 'flex',
46-
alignItems: 'center',
47-
justifyContent: 'center',
48-
height: 'full',
49-
width: controlSize(),
50-
flexShrink: 0,
51-
borderStyle: 'none',
52-
outlineStyle: 'none',
53-
backgroundColor: 'transparent',
54-
padding: 0,
55-
color: 'inherit',
56-
'--iconPrimary': {
57-
type: 'fill',
58-
value: 'currentColor'
59-
}
60-
})({...renderProps, size: props.size || 'M'})}>
61-
<CrossIcon size={props.size || 'M'} />
73+
style={pressScale(domRef)}
74+
className={renderProps => visibleClearButton({...renderProps, size, isStaticColor})}>
75+
<CrossIcon size={props.size} />
6276
</Button>
6377
);
6478
});

packages/@react-spectrum/s2/src/TagGroup.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,13 +502,13 @@ export const Tag = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tag({ch
502502
style={pressScale(domRef)}
503503
className={renderProps => tagStyles({size, isEmphasized, isLink, ...renderProps})} >
504504
{composeRenderProps(children, (children, renderProps) => (
505-
<TagWrapper isInRealDOM={isInRealDOM} {...renderProps}>{typeof children === 'string' ? <Text>{children}</Text> : children}</TagWrapper>
505+
<TagWrapper isInRealDOM={isInRealDOM} isEmphasized={isEmphasized} {...renderProps}>{typeof children === 'string' ? <Text>{children}</Text> : children}</TagWrapper>
506506
))}
507507
</AriaTag>
508508
);
509509
});
510510

511-
function TagWrapper({children, isDisabled, allowsRemoving, isInRealDOM}) {
511+
function TagWrapper({children, isDisabled, allowsRemoving, isInRealDOM, isEmphasized, isSelected}) {
512512
let {size = 'M'} = useSlottedContext(TagGroupContext) ?? {};
513513
return (
514514
<>
@@ -553,6 +553,7 @@ function TagWrapper({children, isDisabled, allowsRemoving, isInRealDOM}) {
553553
<ClearButton
554554
slot="remove"
555555
size={size}
556+
isStaticColor={isEmphasized && isSelected}
556557
isDisabled={isDisabled} />
557558
)}
558559
</>

packages/@react-spectrum/s2/stories/TagGroup.stories.tsx

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ export default meta;
5050

5151
export let Example = {
5252
render: (args: any) => {
53+
let props = {...args};
5354
if (args.onRemove) {
54-
args.onRemove = action('remove');
55+
props.onRemove = action('remove');
5556
}
5657
return (
5758
<div style={{width: 320, resize: 'horizontal', overflow: 'hidden', padding: 4}}>
58-
<TagGroup {...args}>
59+
<TagGroup {...props}>
5960
<Tag id="chocolate">Chocolate</Tag>
6061
<Tag>Mint</Tag>
6162
<Tag>Strawberry</Tag>
@@ -102,12 +103,13 @@ let items: Array<ITagItem> = [
102103
];
103104
export let Dynamic = {
104105
render: (args: any) => {
106+
let props = {...args};
105107
if (args.onRemove) {
106-
args.onRemove = action('remove');
108+
props.onRemove = action('remove');
107109
}
108110
return (
109111
<div style={{width: 320, resize: 'horizontal', overflow: 'hidden', padding: 4}}>
110-
<TagGroup {...args} items={items}>
112+
<TagGroup {...props} items={items}>
111113
{(item: ITagItem) => <Tag>{item.name}</Tag>}
112114
</TagGroup>
113115
</div>
@@ -125,12 +127,13 @@ const SRC_URL_1 =
125127

126128
export let Disabled = {
127129
render: (args: any) => {
130+
let props = {...args};
128131
if (args.onRemove) {
129-
args.onRemove = action('remove');
132+
props.onRemove = action('remove');
130133
}
131134

132135
return (
133-
<TagGroup {...args} disabledKeys={new Set(['mint', 'vanilla'])} styles={style({width: 320})}>
136+
<TagGroup {...props} disabledKeys={new Set(['mint', 'vanilla'])} styles={style({width: 320})}>
134137
<Tag id="chocolate" textValue="chocolate"><NewIcon /><Text>Chocolate</Text></Tag>
135138
<Tag id="mint">Mint</Tag>
136139
<Tag id="strawberry">
@@ -165,12 +168,13 @@ function renderEmptyState() {
165168
}
166169
export let Empty = {
167170
render: (args: any) => {
171+
let props = {...args};
168172
if (args.onRemove) {
169-
args.onRemove = action('remove');
173+
props.onRemove = action('remove');
170174
}
171175

172176
return (
173-
<TagGroup {...args} renderEmptyState={renderEmptyState} />
177+
<TagGroup {...props} renderEmptyState={renderEmptyState} />
174178
);
175179
},
176180
args: {
@@ -179,12 +183,13 @@ export let Empty = {
179183
};
180184
export let DefaultEmpty = {
181185
render: (args: any) => {
186+
let props = {...args};
182187
if (args.onRemove) {
183-
args.onRemove = action('remove');
188+
props.onRemove = action('remove');
184189
}
185190

186191
return (
187-
<TagGroup {...args} />
192+
<TagGroup {...props} />
188193
);
189194
},
190195
args: {
@@ -194,8 +199,12 @@ export let DefaultEmpty = {
194199

195200
export let Links = {
196201
render: (args: any) => {
202+
let props = {...args};
203+
if (args.onRemove) {
204+
props.onRemove = action('remove');
205+
}
197206
return (
198-
<TagGroup {...args} disabledKeys={new Set(['google'])}>
207+
<TagGroup {...props} disabledKeys={new Set(['google'])}>
199208
<Tag id="adobe" href="https://adobe.com">Adobe</Tag>
200209
<Tag id="google">Google</Tag>
201210
<Tag id="apple" href="https://apple.com">Apple</Tag>
@@ -210,12 +219,13 @@ export let Links = {
210219

211220
export const ContextualHelpExample = {
212221
render: (args: any) => {
222+
let props = {...args};
213223
if (args.onRemove) {
214-
args.onRemove = action('remove');
224+
props.onRemove = action('remove');
215225
}
216226
return (
217227
<TagGroup
218-
{...args}
228+
{...props}
219229
contextualHelp={
220230
<ContextualHelp>
221231
<Heading>What is a ice cream?</Heading>

packages/@react-spectrum/tag/src/Tag.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function Tag<T>(props: SpectrumTagProps<T>): ReactNode {
3535
// @ts-ignore
3636
let {styleProps} = useStyleProps(otherProps);
3737
let {hoverProps, isHovered} = useHover({});
38-
let {isFocused, isFocusVisible, focusProps} = useFocusRing({within: true});
38+
let {isFocused, isFocusVisible, focusProps} = useFocusRing({within: false});
3939
let ref = useRef(null);
4040
let {removeButtonProps, gridCellProps, rowProps, allowsRemoving} = useTag({
4141
...props,
@@ -81,7 +81,7 @@ function TagRemoveButton(props) {
8181

8282
return (
8383
<span {...styleProps}>
84-
<ClearButton {...props} />
84+
<ClearButton {...props} inset />
8585
</span>
8686
);
8787
}

packages/react-aria-components/docs/TagGroup.mdx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,15 +288,23 @@ function Example() {
288288
background: none;
289289
border: none;
290290
padding: 0;
291-
margin-left: 8px;
291+
margin-left: 2px;
292292
color: var(--text-color-base);
293293
transition: color 200ms;
294294
outline: none;
295295
font-size: 0.95em;
296+
border-radius: 100%;
297+
aspect-ratio: 1/1;
298+
height: 100%;
296299

297300
&[data-hovered] {
298301
color: var(--text-color-hover);
299302
}
303+
304+
&[data-focus-visible] {
305+
outline: 2px solid var(--focus-ring-color);
306+
outline-offset: -1px;
307+
}
300308
}
301309

302310
&[data-selected] {

0 commit comments

Comments
 (0)