-
Notifications
You must be signed in to change notification settings - Fork 91
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(theming): add focus styling utilities #1542
Changes from all commits
c7a0e27
40519db
60d8035
8bf37a0
9208984
8cf8142
3ba04b3
4919098
9a924b1
67facad
bc65fc4
290b4f6
d1ea6a3
bda673c
c01082e
c359ba2
d0a1177
12c1f19
fcdfd95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,21 @@ | ||
{ | ||
"index.cjs.js": { | ||
"bundled": 21035, | ||
"minified": 13433, | ||
"gzipped": 5155 | ||
}, | ||
"index.esm.js": { | ||
"bundled": 18296, | ||
"minified": 11411, | ||
"gzipped": 4620, | ||
"bundled": 20042, | ||
"minified": 12530, | ||
"gzipped": 5047, | ||
"treeshaked": { | ||
"rollup": { | ||
"code": 1819, | ||
"import_statements": 146 | ||
"code": 3933, | ||
"import_statements": 216 | ||
}, | ||
"webpack": { | ||
"code": 2388 | ||
"code": 4201 | ||
} | ||
} | ||
}, | ||
"index.cjs.js": { | ||
"bundled": 19098, | ||
"minified": 12154, | ||
"gzipped": 4707 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
/** | ||
* Copyright Zendesk, Inc. | ||
* | ||
* Use of this source code is governed under the Apache License, Version 2.0 | ||
* found at http://www.apache.org/licenses/LICENSE-2.0. | ||
*/ | ||
|
||
import React from 'react'; | ||
import { render } from 'garden-test-utils'; | ||
import styled, { ThemeProps, DefaultTheme, CSSObject } from 'styled-components'; | ||
import { focusStyles } from './focusStyles'; | ||
import { Hue } from './getColor'; | ||
import DEFAULT_THEME from '../elements/theme'; | ||
import PALETTE from '../elements/palette'; | ||
|
||
interface IStyledDivProps extends ThemeProps<DefaultTheme> { | ||
condition?: boolean; | ||
inset?: boolean; | ||
hue?: Hue; | ||
selector?: string; | ||
shade?: number; | ||
shadowWidth?: 'sm' | 'md'; | ||
spacerWidth?: null | 'xs' | 'sm'; | ||
styles?: CSSObject; | ||
} | ||
|
||
const StyledDiv = styled.div<IStyledDivProps>` | ||
${props => focusStyles(props)} | ||
`; | ||
|
||
describe('focusStyles', () => { | ||
it('renders with expected defaults', () => { | ||
const { container } = render(<StyledDiv />); | ||
const expected = `${DEFAULT_THEME.shadowWidths.md} ${PALETTE.blue[600]}`; | ||
|
||
expect(container.firstChild).toHaveStyleRule('outline', 'none', { modifier: '&:focus' }); | ||
expect(container.firstChild).toHaveStyleRule('box-shadow', expect.stringContaining(expected), { | ||
modifier: '&:focus-visible' | ||
}); | ||
expect(container.firstChild).toHaveStyleRule('box-shadow', expect.stringContaining(expected), { | ||
modifier: "&[data-garden-focus-visible='true']" | ||
}); | ||
expect(container.firstChild).toHaveStyleRule( | ||
'outline', | ||
expect.stringContaining('2px solid transparent'), | ||
{ | ||
modifier: '&:focus-visible' | ||
} | ||
); | ||
expect(container.firstChild).toHaveStyleRule( | ||
'outline-offset', | ||
expect.stringContaining(`${DEFAULT_THEME.shadowWidths.xs}`), | ||
{ | ||
modifier: '&:focus-visible' | ||
} | ||
); | ||
}); | ||
|
||
it('renders inset as expected', () => { | ||
const { container } = render(<StyledDiv inset />); | ||
|
||
expect(container.firstChild).toHaveStyleRule('box-shadow', expect.stringContaining('inset'), { | ||
modifier: '&:focus-visible' | ||
}); | ||
}); | ||
|
||
it('renders color as expected', () => { | ||
const { container } = render(<StyledDiv hue="red" shade={400} />); | ||
|
||
expect(container.firstChild).toHaveStyleRule( | ||
'box-shadow', | ||
expect.stringContaining(`${DEFAULT_THEME.shadowWidths.md} ${PALETTE.red[400]}`), | ||
{ | ||
modifier: '&:focus-visible' | ||
} | ||
); | ||
}); | ||
|
||
it('renders selector as expected', () => { | ||
const { container } = render(<StyledDiv selector="&:focus-within" />); | ||
|
||
expect(container.firstChild).toHaveStyleRule( | ||
'box-shadow', | ||
expect.stringContaining(`${PALETTE.blue[600]}`), | ||
{ | ||
modifier: '&:focus-within' | ||
} | ||
); | ||
}); | ||
|
||
it('renders size as expected', () => { | ||
const { container } = render(<StyledDiv shadowWidth="sm" spacerWidth="sm" />); | ||
|
||
expect(container.firstChild).toHaveStyleRule( | ||
'box-shadow', | ||
expect.stringContaining(`${DEFAULT_THEME.shadowWidths.sm} ${PALETTE.blue[600]}`), | ||
{ | ||
modifier: '&:focus-visible' | ||
} | ||
); | ||
expect(container.firstChild).toHaveStyleRule( | ||
'box-shadow', | ||
expect.stringContaining(`${DEFAULT_THEME.shadowWidths.sm} ${PALETTE.white}`), | ||
{ | ||
modifier: '&:focus-visible' | ||
} | ||
); | ||
}); | ||
|
||
it('conditionally renders without `box-shadow`', () => { | ||
const { container } = render(<StyledDiv condition={false} />); | ||
|
||
expect(container.firstChild).not.toHaveStyleRule('box-shadow', undefined, { | ||
modifier: '&:focus-visible' | ||
}); | ||
}); | ||
|
||
it('knocks out spacer as expected', () => { | ||
const { container } = render(<StyledDiv spacerWidth={null} />); | ||
|
||
expect(container.firstChild).not.toHaveStyleRule('outline-offset', undefined, { | ||
modifier: '&:focus-visible' | ||
}); | ||
}); | ||
|
||
it('renders user provided styles', () => { | ||
const { container } = render( | ||
<StyledDiv styles={{ backgroundColor: 'black', color: 'white' }} /> | ||
); | ||
|
||
expect(container.firstChild).toHaveStyleRule('background-color', 'black', { | ||
modifier: '&:focus-visible' | ||
}); | ||
expect(container.firstChild).toHaveStyleRule('color', 'white', { | ||
modifier: '&:focus-visible' | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,89 @@ | ||||||
/** | ||||||
* Copyright Zendesk, Inc. | ||||||
* | ||||||
* Use of this source code is governed under the Apache License, Version 2.0 | ||||||
* found at http://www.apache.org/licenses/LICENSE-2.0. | ||||||
*/ | ||||||
|
||||||
import { css, CSSObject } from 'styled-components'; | ||||||
import { math } from 'polished'; | ||||||
import { FocusBoxShadowParameters, getFocusBoxShadow } from './getFocusBoxShadow'; | ||||||
|
||||||
export const SELECTOR_FOCUS_VISIBLE = '&:focus-visible, &[data-garden-focus-visible="true"]'; | ||||||
|
||||||
type FocusStylesParameters = FocusBoxShadowParameters & { | ||||||
condition?: boolean; | ||||||
selector?: string; | ||||||
styles?: CSSObject; | ||||||
}; | ||||||
|
||||||
/** | ||||||
* Garden standard `box-shadow` focus styling. | ||||||
* | ||||||
* @param {boolean} [options.condition=true] Supplies an optional condition that can be used to prevent the focus `box-shadow` | ||||||
* @param {boolean} [options.inset=false] Determines whether the `box-shadow` is inset | ||||||
* @param {string|Object} [options.hue='primaryHue'] Provides a theme object `palette` hue or `color` key, or any valid CSS color notation | ||||||
* @param {string} [options.selector=SELECTOR_FOCUS_VISIBLE] Provides a subsitute `:focus-visible` pseudo-class CSS selector. | ||||||
* @param {number} [options.shade=600] Selects a shade for the given hue | ||||||
* @param {string} [options.shadowWidth='md'] Provides a theme object `shadowWidth` key for the cumulative width of the `box-shadow` | ||||||
* @param {string} [options.spacerWidth='xs'] Provides a theme object `shadowWidth` for the white spacer, or `null` to remove | ||||||
* @param {Object} [options.styles] Adds CSS property values to be rendered with `:focus-visible` | ||||||
* @param {Object} options.theme Provides values used to resolve the desired color | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Assuming the brackets were left out on accident. :) |
||||||
* | ||||||
* @returns CSS structured as follows, with `{values}` determined by the options provided: | ||||||
* ```css | ||||||
* :focus { | ||||||
* outline: none; | ||||||
* } | ||||||
* | ||||||
* :focus-visible, | ||||||
* [data-garden-focus-visible='true'] { | ||||||
* box-shadow: 0 0 0 {1px} #fff, | ||||||
* 0 0 0 {3px} {blue}; | ||||||
* outline: {2px} solid transparent; | ||||||
* outline-offset: {1px}; | ||||||
* // additional {styles} here... | ||||||
* } | ||||||
* ``` | ||||||
*/ | ||||||
export const focusStyles = ({ | ||||||
condition = true, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious, what advantage does the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See commit linked in description |
||||||
selector = SELECTOR_FOCUS_VISIBLE, | ||||||
shadowWidth = 'md', | ||||||
spacerWidth = 'xs', | ||||||
styles, | ||||||
theme, | ||||||
...options | ||||||
}: FocusStylesParameters) => { | ||||||
const boxShadow = condition | ||||||
? getFocusBoxShadow({ shadowWidth, spacerWidth, theme, ...options }) | ||||||
: undefined; | ||||||
let outline; | ||||||
let outlineOffset; | ||||||
|
||||||
if (spacerWidth === null) { | ||||||
outline = theme.shadowWidths[shadowWidth]; | ||||||
} else { | ||||||
outline = `${math( | ||||||
`${theme.shadowWidths[shadowWidth]} - ${theme.shadowWidths[spacerWidth]}` | ||||||
)} solid transparent`; | ||||||
outlineOffset = theme.shadowWidths[spacerWidth]; | ||||||
} | ||||||
|
||||||
/* | ||||||
* 1. Browser reset | ||||||
* 2. High contrast mode hack | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See link in description There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for transparent. |
||||||
*/ | ||||||
return css` | ||||||
&:focus { | ||||||
outline: none; /* [1] */ | ||||||
} | ||||||
|
||||||
${selector} { | ||||||
outline: ${outline}; /* [2] */ | ||||||
outline-offset: ${outlineOffset}; | ||||||
box-shadow: ${boxShadow}; | ||||||
${styles} | ||||||
} | ||||||
`; | ||||||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:focus-visible
π π