diff --git a/.github/workflows/approve-dependabot.yml b/.github/workflows/approve-dependabot.yml new file mode 100644 index 00000000000..03567f0bcd2 --- /dev/null +++ b/.github/workflows/approve-dependabot.yml @@ -0,0 +1,13 @@ +name: Approve dependabot pull requests +on: [pull_request] +jobs: + automate-pullrequest-review: + runs-on: ubuntu-latest + steps: + - name: Approve dependabot pull request + if: github.actor == 'dependabot' + uses: andrewmusgrave/automatic-pull-request-review@0.0.2 + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' + event: APPROVE + body: 'Thank you dependabot 🎊' diff --git a/UNRELEASED.md b/UNRELEASED.md index d73e0d09805..16da49ca9ba 100644 --- a/UNRELEASED.md +++ b/UNRELEASED.md @@ -11,15 +11,28 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f ### Enhancements - Changed border color of `Drop zone` to have better contrast from the background and to be lighter when disabled ([#2119](https://github.com/Shopify/polaris-react/pull/2119)) +- Adjusted search results overlay to take up 100% height of the screen on small screens and to match the width of the search bar on large screens. ([#2103](https://github.com/Shopify/polaris-react/pull/2103)) +- Added skipToContentTarget prop to Frame component ([#2080](https://github.com/Shopify/polaris-react/pull/2080)) ### Bug fixes +- Fixed vertical alignment of Tabs disclosure activator ([#2087](https://github.com/Shopify/polaris-react/pull/2087)) +- Fixed `Modal` setting an invalid `id` on `aria-labelledby` when no `title` is set ([#2115](https://github.com/Shopify/polaris-react/pull/2115)) +- Fixed error warnings in `Card` and `RollupActions` tests ([#2125](https://github.com/Shopify/polaris-react/pull/2125)) +- Added default accessibility label from `ResourceItem` ([#2097](https://github.com/Shopify/polaris-react/pull/2097)) + ### Documentation +- Updated the `withContext` section in the [v3 to v4 migration guide](https://github.com/Shopify/polaris-react/blob/master/documentation/guides/migrating-from-v3-to-v4.md) ([#2124](https://github.com/Shopify/polaris-react/pull/2124)) + ### Development workflow ### Dependency upgrades ### Code quality +- Migrated `ContextualSaveBar` to use hooks instead of `withAppProvider` ([#2091](https://github.com/Shopify/polaris-react/pull/2091)) +- Migrated `RangeSlider`, `ScrollLock` and `TopBar.SearchField` to use hooks instead of withAppProvider ([#2083](https://github.com/Shopify/polaris-react/pull/2083)) +- Updated `ResourceItem` to no longer rely on withAppProvider ([#2094](https://github.com/Shopify/polaris-react/pull/2094)) + ### Deprecations diff --git a/a11y_shitlist.json b/a11y_shitlist.json deleted file mode 100644 index b53b472b144..00000000000 --- a/a11y_shitlist.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "all-components-app-provider--with-i18n": [ - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", - "context": "", - "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", - "type": "error", - "typeCode": 1, - "selector": "#ResourceListItemOverlay1" - }, - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", - "context": "", - "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", - "type": "error", - "typeCode": 1, - "selector": "#ResourceListItemOverlay2" - } - ], - "all-components-app-provider--default": [ - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", - "context": "", - "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", - "type": "error", - "typeCode": 1, - "selector": "#ResourceListItemOverlay1" - }, - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", - "context": "", - "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", - "type": "error", - "typeCode": 1, - "selector": "#ResourceListItemOverlay2" - } - ], - "all-components-popover--popover-with-lazy-loaded-list": [ - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.Button.Name", - "context": "", - "message": "This button element does not have a name available to an accessibility API. Valid names are: title undefined, element content, aria-label undefined, aria-labelledby undefined.", - "type": "error", - "typeCode": 1, - "selector": "#Popover1 > div > div > ul > li:nth-child(1) > div > button" - }, - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.Button.Name", - "context": "", - "message": "This button element does not have a name available to an accessibility API. Valid names are: title undefined, element content, aria-label undefined, aria-labelledby undefined.", - "type": "error", - "typeCode": 1, - "selector": "#Popover1 > div > div > ul > li:nth-child(2) > div > button" - }, - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.Button.Name", - "context": "", - "message": "This button element does not have a name available to an accessibility API. Valid names are: title undefined, element content, aria-label undefined, aria-labelledby undefined.", - "type": "error", - "typeCode": 1, - "selector": "#Popover1 > div > div > ul > li:nth-child(3) > div > button" - }, - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.Button.Name", - "context": "", - "message": "This button element does not have a name available to an accessibility API. Valid names are: title undefined, element content, aria-label undefined, aria-labelledby undefined.", - "type": "error", - "typeCode": 1, - "selector": "#Popover1 > div > div > ul > li:nth-child(4) > div > button" - }, - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.Button.Name", - "context": "", - "message": "This button element does not have a name available to an accessibility API. Valid names are: title undefined, element content, aria-label undefined, aria-labelledby undefined.", - "type": "error", - "typeCode": 1, - "selector": "#Popover1 > div > div > ul > li:nth-child(5) > div > button" - } - ], - "all-components-resource-list--resource-list-with-filtering": [ - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", - "context": "", - "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", - "type": "error", - "typeCode": 1, - "selector": "#ResourceListItemOverlay1" - }, - { - "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", - "context": "", - "message": "Anchor element found with a valid href attribute, but no link content has been supplied.", - "type": "error", - "typeCode": 1, - "selector": "#ResourceListItemOverlay2" - } - ] -} diff --git a/documentation/guides/migrating-from-v3-to-v4.md b/documentation/guides/migrating-from-v3-to-v4.md index d343fc10c05..bc9367b7659 100644 --- a/documentation/guides/migrating-from-v3-to-v4.md +++ b/documentation/guides/migrating-from-v3-to-v4.md @@ -237,7 +237,7 @@ The `Tabs.Panel` subcomponent has been removed. This was an undocumented subcomp ### WithContext -The `WithContext` component has been removed. It was used as a utility to handle multiple [legacy contexts](https://reactjs.org/docs/legacy-context.html) at once. Use [modern contexts](https://reactjs.org/docs/context.html#api) and access them using providers, hooks or `Class.contextType` instead. +The `WithContext` component has been removed. It was used as a utility to handle [legacy contexts](https://reactjs.org/docs/legacy-context.html) in class based components and multiple contexts at once. Use [modern contexts](https://reactjs.org/docs/context.html#api) and access them using providers, hooks, or `Class.contextType` instead. ```jsx // old diff --git a/locales/de.json b/locales/de.json index 114639e2308..a2d34fc5a57 100644 --- a/locales/de.json +++ b/locales/de.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} Produkte", "Item": { "actionsDropdownLabel": "Aktionen für {accessibilityLabel}", - "actionsDropdown": "Dropdown-Liste mit Aktionen" + "actionsDropdown": "Dropdown-Liste mit Aktionen", + "viewItem": "Details anzeigen für {itemName}" }, "BulkActions": { "actionsActivatorLabel": "Aktionen", diff --git a/locales/en.json b/locales/en.json index 74774b6beb6..37cc2dd866d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -170,7 +170,8 @@ "Item": { "actionsDropdownLabel": "Actions for {accessibilityLabel}", - "actionsDropdown": "Actions dropdown" + "actionsDropdown": "Actions dropdown", + "viewItem": "View details for {itemName}" }, "BulkActions": { diff --git a/locales/es.json b/locales/es.json index 87fa46c9172..4d11a6b6e86 100644 --- a/locales/es.json +++ b/locales/es.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} artículos", "Item": { "actionsDropdownLabel": "Acciones para {accessibilityLabel}", - "actionsDropdown": "Menú desplegable de acciones" + "actionsDropdown": "Menú desplegable de acciones", + "viewItem": "Ver detalles de {itemName}" }, "BulkActions": { "actionsActivatorLabel": "Acciones", diff --git a/locales/fr.json b/locales/fr.json index eb448e5692c..1219315a900 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} articles", "Item": { "actionsDropdownLabel": "Actions pour {accessibilityLabel}", - "actionsDropdown": "Actions de la liste déroulante" + "actionsDropdown": "Actions de la liste déroulante", + "viewItem": "Afficher les détails de {itemName}" }, "BulkActions": { "actionsActivatorLabel": "Actions", diff --git a/locales/hi.json b/locales/hi.json index 3dbfc7348d2..e2fef7fedf8 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} आइटम", "Item": { "actionsDropdownLabel": "{accessibilityLabel} के लिए कार्रवाई", - "actionsDropdown": "कार्रवाई ड्रॉपडाउन" + "actionsDropdown": "कार्रवाई ड्रॉपडाउन", + "viewItem": "{itemName} के लिए विवरण देखें" }, "BulkActions": { "actionsActivatorLabel": "कार्रवाई", diff --git a/locales/it.json b/locales/it.json index bdb0293db8b..555526079d7 100644 --- a/locales/it.json +++ b/locales/it.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} articoli", "Item": { "actionsDropdownLabel": "Azioni per {accessibilityLabel}", - "actionsDropdown": "Menu a tendina delle azioni" + "actionsDropdown": "Menu a tendina delle azioni", + "viewItem": "Visualizza dettagli di {itemName}" }, "BulkActions": { "actionsActivatorLabel": "Azioni", diff --git a/locales/ja.json b/locales/ja.json index 7940dc1e9c8..65ae166b9ad 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength}個のアイテム", "Item": { "actionsDropdownLabel": "{accessibilityLabel}のアクション", - "actionsDropdown": "アクションドロップダウン" + "actionsDropdown": "アクションドロップダウン", + "viewItem": "{itemName}の詳細を表示する" }, "BulkActions": { "actionsActivatorLabel": "アクション", diff --git a/locales/ms.json b/locales/ms.json index 7ec66f6bccc..620de1e8a48 100644 --- a/locales/ms.json +++ b/locales/ms.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} item", "Item": { "actionsDropdownLabel": "Tindakan untuk {accessibilityLabel}", - "actionsDropdown": "Juntai bawah tindakan" + "actionsDropdown": "Juntai bawah tindakan", + "viewItem": "Lihat butiran untuk {itemName}" }, "BulkActions": { "actionsActivatorLabel": "Tindakan", diff --git a/locales/nl.json b/locales/nl.json index 235c4f27018..18cd621834f 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} artikelen", "Item": { "actionsDropdownLabel": "Acties voor {accessibilityLabel}", - "actionsDropdown": "Vervolgkeuze acties" + "actionsDropdown": "Vervolgkeuze acties", + "viewItem": "Details weergeven voor {itemName}" }, "BulkActions": { "actionsActivatorLabel": "Acties", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 6d17cd6e7b1..0caaeb954e6 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} itens", "Item": { "actionsDropdownLabel": "Ações para {accessibilityLabel}", - "actionsDropdown": "Menu suspenso Ações" + "actionsDropdown": "Menu suspenso Ações", + "viewItem": "Visualizar detalhes de {itemName}" }, "BulkActions": { "actionsActivatorLabel": "Ações", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 7a373fd68c9..059cf3cf998 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} 件产品", "Item": { "actionsDropdownLabel": "{accessibilityLabel}的操作", - "actionsDropdown": "操作下拉菜单" + "actionsDropdown": "操作下拉菜单", + "viewItem": "查看 {itemName} 详细信息" }, "BulkActions": { "actionsActivatorLabel": "编辑", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index f4c5de1f3fc..53bc1bf81ef 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -150,7 +150,8 @@ "ariaLivePlural": "{itemsLength} 件商品", "Item": { "actionsDropdownLabel": "{accessibilityLabel} 的動作", - "actionsDropdown": "動作下拉式選單" + "actionsDropdown": "動作下拉式選單", + "viewItem": "檢視 {itemName} 的詳細資訊" }, "BulkActions": { "actionsActivatorLabel": "動作", diff --git a/scripts/pa11y-utilities.js b/scripts/pa11y-utilities.js deleted file mode 100644 index 0664c24c2ae..00000000000 --- a/scripts/pa11y-utilities.js +++ /dev/null @@ -1,45 +0,0 @@ -const hash = require('object-hash'); - -function shitlistCheck(results, immutableShitlist) { - const mutableShitlist = {}; - const remainingIssues = []; - Object.keys(immutableShitlist).forEach((key) => { - mutableShitlist[key] = Array.from(immutableShitlist[key]); - }); - - const filteredResults = results.map((result) => { - if (mutableShitlist[result.exampleID]) { - result.issues = result.issues.filter((issue) => { - const issueHash = hash(issue); - const matchIndex = mutableShitlist[result.exampleID].findIndex( - (shitlistedResult) => { - return hash(shitlistedResult) === issueHash; - }, - ); - if (matchIndex >= 0) { - mutableShitlist[result.exampleID].splice(matchIndex, 1); - } - return matchIndex === -1; - }); - } - return result; - }); - - Object.keys(mutableShitlist).forEach((key) => { - if (mutableShitlist[key].length) { - remainingIssues.push({ - exampleID: key, - issues: mutableShitlist[key], - }); - } - }); - - return { - results: filteredResults.filter((result) => result.issues.length), - remainingIssues: Object.keys(remainingIssues).length - ? remainingIssues - : null, - }; -} - -module.exports.shitlistCheck = shitlistCheck; diff --git a/scripts/pa11y.js b/scripts/pa11y.js index 7458c9ae9c2..9b0f6f623d2 100644 --- a/scripts/pa11y.js +++ b/scripts/pa11y.js @@ -1,9 +1,6 @@ /* eslint-disable no-console */ const puppeteer = require('puppeteer'); const pa11y = require('pa11y'); -const shitlistCheck = require('./pa11y-utilities.js').shitlistCheck; - -const shitlist = require('./../a11y_shitlist.json'); const NUMBER_OF_BROWSERS = 5; @@ -106,28 +103,7 @@ async function runPa11y() { process.exit(1); } - const {results, remainingIssues} = shitlistCheck(rawResults, shitlist); - - if (remainingIssues) { - console.log( - ` -======================================================================== -The following items were fixed, and therefore should be removed from the shitlist. -Please edit the file a11y_shitlist.json to remove them and run these tests again.', -======================================================================== -`, - ); - remainingIssues.forEach((issue) => { - console.log( - '------------------------------------------------------------------------', - ); - console.log(issue.exampleID); - console.log( - '------------------------------------------------------------------------', - ); - console.log(JSON.stringify(issue.issues, null, 2)); - }); - } + const results = rawResults.filter((result) => result.issues.length); console.log( ` @@ -153,7 +129,7 @@ The following issues were discovered and need to be fixed before this code can b console.log('No issues!'); } - if (results.length || remainingIssues) { + if (results.length) { process.exit(1); } process.exit(0); diff --git a/src/components/ActionMenu/components/RollupActions/tests/RollupActions.test.tsx b/src/components/ActionMenu/components/RollupActions/tests/RollupActions.test.tsx index a1db3f9e5b9..ace7e188f0b 100644 --- a/src/components/ActionMenu/components/RollupActions/tests/RollupActions.test.tsx +++ b/src/components/ActionMenu/components/RollupActions/tests/RollupActions.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {ReactWrapper} from 'enzyme'; import {HorizontalDotsMinor} from '@shopify/polaris-icons'; -import {mountWithAppProvider, trigger} from 'test-utilities/legacy'; +import {mountWithAppProvider, trigger, act} from 'test-utilities/legacy'; import {Button, Popover} from 'components'; @@ -60,7 +60,10 @@ describe('', () => { expect(popoverComponent.prop('active')).toBe(true); const firstActionListItem = wrapper.find(ActionListItem).first(); - trigger(firstActionListItem, 'onAction'); + act(() => { + trigger(firstActionListItem, 'onAction'); + }); + popoverComponent.update(); popoverComponent = wrapper.find(Popover); expect(popoverComponent.prop('active')).toBe(false); @@ -107,5 +110,8 @@ function findPopoverActivator(wrapper: Wrapper) { function activatePopover(wrapper: Wrapper) { const activator = findPopoverActivator(wrapper); - trigger(activator, 'onClick'); + act(() => { + trigger(activator, 'onClick'); + }); + wrapper.update(); } diff --git a/src/components/AppProvider/README.md b/src/components/AppProvider/README.md index 6af878df6a3..0d657cb6761 100644 --- a/src/components/AppProvider/README.md +++ b/src/components/AppProvider/README.md @@ -46,6 +46,9 @@ AppProvider works by default without any additional options passed to it. defaultItemSingular: 'item', defaultItemPlural: 'items', showing: 'Showing {itemsCount} {resource}', + Item: { + viewItem: 'View details for {itemName}', + }, }, Common: { checkbox: 'checkbox', @@ -106,6 +109,9 @@ With an `i18n`, `AppProvider` will provide these translations to polaris compone showing: '{itemsCount} {resource} affichés', defaultItemPlural: 'articles', defaultItemSingular: 'article', + Item: { + viewItem: "Afficher les détails de l'{itemName}", + }, }, }, }} diff --git a/src/components/Card/tests/Card.test.tsx b/src/components/Card/tests/Card.test.tsx index f2c492201a2..957b7fba6c8 100644 --- a/src/components/Card/tests/Card.test.tsx +++ b/src/components/Card/tests/Card.test.tsx @@ -3,6 +3,7 @@ import { mountWithAppProvider, trigger, findByTestID, + act, } from 'test-utilities/legacy'; import {Card, Badge, Button, Popover, ActionList} from 'components'; import {WithinContentContext} from '../../../utilities/within-content-context'; @@ -140,8 +141,10 @@ describe('', () => { expect(popover).toHaveLength(1); expect(popover.prop('active')).toBe(false); - trigger(disclosureButton, 'onClick'); - + act(() => { + trigger(disclosureButton, 'onClick'); + }); + card.update(); expect( card .find(Popover) diff --git a/src/components/Frame/Frame.tsx b/src/components/Frame/Frame.tsx index 969241895b1..4c74d2502c2 100644 --- a/src/components/Frame/Frame.tsx +++ b/src/components/Frame/Frame.tsx @@ -43,6 +43,8 @@ export interface FrameProps { * @default false */ showMobileNavigation?: boolean; + /** Accepts a ref to the html anchor element you wish to focus when clicking the skip to content link */ + skipToContentTarget?: React.RefObject; /** A callback function to handle clicking the mobile navigation dismiss button */ onNavigationDismiss?(): void; } @@ -81,7 +83,8 @@ class Frame extends React.PureComponent { private contextualSaveBar: ContextualSaveBarProps | null; private globalRibbonContainer: HTMLDivElement | null = null; private navigationNode = createRef(); - private skipToMainContentTargetNode = React.createRef(); + private skipToMainContentTargetNode = + this.props.skipToContentTarget || React.createRef(); componentDidMount() { this.handleResize(); @@ -112,6 +115,7 @@ class Frame extends React.PureComponent { globalRibbon, showMobileNavigation = false, polaris: {intl}, + skipToContentTarget, } = this.props; const navClassName = classNames( @@ -203,10 +207,14 @@ class Frame extends React.PureComponent { skipFocused && styles.focused, ); + const skipTarget = skipToContentTarget + ? (skipToContentTarget.current && skipToContentTarget.current.id) || '' + : APP_FRAME_MAIN_ANCHOR_TARGET; + const skipMarkup = (
{ /> ) : null; - const skipToMainContentTarget = ( + const skipToMainContentTarget = skipToContentTarget ? null : ( // eslint-disable-next-line jsx-a11y/anchor-is-valid : null; + const skipToContentTarget = ( + + ); + const actualPageMarkup = ( + {skipToContentTarget} {contextualSaveBarMarkup} {loadingMarkup} diff --git a/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx b/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx index 95918f65d62..1b1aa43024b 100644 --- a/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx +++ b/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx @@ -1,13 +1,12 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {getWidth} from '../../../../utilities/get-width'; import {ContextualSaveBarProps} from '../../../../utilities/frame'; -import { - withAppProvider, - WithAppProviderProps, -} from '../../../../utilities/with-app-provider'; import {Button} from '../../../Button'; +import {useI18n} from '../../../../utilities/i18n'; +import {useTheme} from '../../../../utilities/theme'; +import {useForcibleToggle} from '../../../../utilities/use-toggle'; import {Image} from '../../../Image'; import {Stack} from '../../../Stack'; @@ -15,131 +14,109 @@ import {DiscardConfirmationModal} from './components'; import styles from './ContextualSaveBar.scss'; -type CombinedProps = ContextualSaveBarProps & WithAppProviderProps; - -interface State { - discardConfirmationModalVisible: boolean; -} - -class ContextualSaveBar extends React.PureComponent { - state: State = { - discardConfirmationModalVisible: false, - }; - - render() { - const {discardConfirmationModalVisible} = this.state; - - const { - alignContentFlush, - message, - discardAction, - saveAction, - polaris: {theme, intl}, - } = this.props; - const logo = theme && theme.logo; - - const discardActionContent = - discardAction && discardAction.content - ? discardAction.content - : intl.translate('Polaris.ContextualSaveBar.discard'); - - let discardActionHandler; - if (discardAction && discardAction.discardConfirmationModal) { - discardActionHandler = this.toggleDiscardConfirmationModal; - } else if (discardAction) { - discardActionHandler = discardAction.onAction; +export function ContextualSaveBar({ + alignContentFlush, + message, + saveAction, + discardAction, +}: ContextualSaveBarProps) { + const i18n = useI18n(); + const {logo} = useTheme(); + + const [ + discardConfirmationModalVisible, + { + toggle: toggleDiscardConfirmationModal, + forceFalse: closeDiscardConfirmationModal, + }, + ] = useForcibleToggle(false); + + const handleDiscardAction = useCallback(() => { + if (discardAction && discardAction.onAction) { + discardAction.onAction(); } + closeDiscardConfirmationModal(); + }, [closeDiscardConfirmationModal, discardAction]); + + const discardActionContent = + discardAction && discardAction.content + ? discardAction.content + : i18n.translate('Polaris.ContextualSaveBar.discard'); + + let discardActionHandler; + if (discardAction && discardAction.discardConfirmationModal) { + discardActionHandler = toggleDiscardConfirmationModal; + } else if (discardAction) { + discardActionHandler = discardAction.onAction; + } - const discardConfirmationModalMarkup = discardAction && - discardAction.onAction && - discardAction.discardConfirmationModal && ( - - ); - - const discardActionMarkup = discardAction && ( - - ); - - const saveActionContent = - saveAction && saveAction.content - ? saveAction.content - : intl.translate('Polaris.ContextualSaveBar.save'); - - const saveActionMarkup = saveAction && ( - - ); - - const width = getWidth(logo, 104); - - const imageMarkup = logo && ( - ); - const logoMarkup = alignContentFlush ? null : ( -
- {imageMarkup} -
- ); - - return ( - -
- {logoMarkup} -
-

{message}

-
- - {discardActionMarkup} - {saveActionMarkup} - -
+ const discardActionMarkup = discardAction && ( + + ); + + const saveActionContent = + saveAction && saveAction.content + ? saveAction.content + : i18n.translate('Polaris.ContextualSaveBar.save'); + + const saveActionMarkup = saveAction && ( + + ); + + const width = getWidth(logo, 104); + + const imageMarkup = logo && ( + + ); + + const logoMarkup = alignContentFlush ? null : ( +
+ {imageMarkup} +
+ ); + + return ( + +
+ {logoMarkup} +
+

{message}

+
+ + {discardActionMarkup} + {saveActionMarkup} +
- {discardConfirmationModalMarkup} - - ); - } - - private handleDiscardAction = () => { - const {discardAction} = this.props; - if (discardAction && discardAction.onAction) { - discardAction.onAction(); - } - this.setState({discardConfirmationModalVisible: false}); - }; - - private toggleDiscardConfirmationModal = () => { - this.setState((prevState) => ({ - discardConfirmationModalVisible: !prevState.discardConfirmationModalVisible, - })); - }; +
+ {discardConfirmationModalMarkup} +
+ ); } - -// Use named export once withAppProvider is refactored away -// eslint-disable-next-line import/no-default-export -export default withAppProvider()(ContextualSaveBar); diff --git a/src/components/Frame/components/ContextualSaveBar/index.ts b/src/components/Frame/components/ContextualSaveBar/index.ts index 4e105cade86..1443ea6b788 100644 --- a/src/components/Frame/components/ContextualSaveBar/index.ts +++ b/src/components/Frame/components/ContextualSaveBar/index.ts @@ -1,3 +1 @@ -import ContextualSaveBar from './ContextualSaveBar'; - -export {ContextualSaveBar}; +export {ContextualSaveBar} from './ContextualSaveBar'; diff --git a/src/components/Frame/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx b/src/components/Frame/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx index 81daf1584f9..6beebf9e359 100644 --- a/src/components/Frame/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx +++ b/src/components/Frame/components/ContextualSaveBar/tests/ContextualSaveBar.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import {mountWithAppProvider, trigger} from 'test-utilities/legacy'; +import {act, mountWithAppProvider, trigger} from 'test-utilities/legacy'; import {Button, Image} from 'components'; -import ContextualSaveBar from '../ContextualSaveBar'; +import {ContextualSaveBar} from '../ContextualSaveBar'; import {DiscardConfirmationModal} from '../components'; describe('', () => { @@ -117,7 +117,9 @@ describe('', () => { const discardConfirmationModal = contextualSaveBar.find( DiscardConfirmationModal, ); - trigger(discardConfirmationModal, 'onCancel'); + act(() => { + trigger(discardConfirmationModal, 'onCancel'); + }); expect(discardConfirmationModal.prop('open')).toBe(false); }); @@ -138,7 +140,9 @@ describe('', () => { DiscardConfirmationModal, ); - trigger(discardConfirmationModal, 'onDiscard'); + act(() => { + trigger(discardConfirmationModal, 'onDiscard'); + }); expect(discardAction.onAction).toHaveBeenCalled(); }); diff --git a/src/components/Frame/tests/Frame.test.tsx b/src/components/Frame/tests/Frame.test.tsx index 31b25e62790..7d725ec115d 100644 --- a/src/components/Frame/tests/Frame.test.tsx +++ b/src/components/Frame/tests/Frame.test.tsx @@ -155,6 +155,30 @@ describe('', () => { expect(mainAnchor.getDOMNode()).toBe(document.activeElement); }); + it('sets focus to target element when the skip to content link is clicked', () => { + const targetId = 'SkipToContentTarget'; + const targetRef = React.createRef(); + + const skipToContentTarget = ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid +
+ ); + + const frame = mountWithAppProvider( + {skipToContentTarget}, + ); + + const triggerAnchor = frame.find('a').at(0); + const targetAnchor = frame.find(`#${targetId}`); + trigger(triggerAnchor, 'onFocus'); + trigger(triggerAnchor, 'onClick'); + + expect(triggerAnchor.getDOMNode().getAttribute('href')).toBe( + `#${targetId}`, + ); + expect(targetAnchor.getDOMNode()).toBe(document.activeElement); + }); + it('renders with a has nav data attribute when nav is passed', () => { const navigation =
; const frame = mountWithAppProvider(); diff --git a/src/components/Link/README.md b/src/components/Link/README.md index b31a1a05081..878a40b8bc3 100644 --- a/src/components/Link/README.md +++ b/src/components/Link/README.md @@ -94,7 +94,9 @@ Use for text links that are the same color as the surrounding text. ``` -Monochrome styles will be applied to links rendered within a `Banner` +### Monochrome link in a banner + +Monochrome styles will be applied to links rendered within a `Banner`. ```jsx @@ -154,13 +156,27 @@ To provide consistency and clarity: fulfilling orders ``` -#### Don’t + + + + +#### Do ```jsx +/* Somewhere in the code: */ +fulfilling orders + +/* Elsewhere in the code: */ fulfilling orders ``` +#### Don’t + ```jsx +/* Somewhere in the code: */ +fulfilling orders + +/* Elsewhere in the code: */ order fulfillment section ``` diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 539420c1627..e7b4f50a402 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -283,10 +283,12 @@ class Modal extends React.Component { /> ); + const labelledBy = title ? this.headerId : undefined; + dialog = ( ` interface Props extends RangeSliderProps {} -type CombinedProps = Props & WithAppProviderProps; -const getUniqueID = createUniqueIDFactory('RangeSlider'); - -class RangeSlider extends React.Component { - private id = getUniqueID(); - - render() { - const { - min = RangeSliderDefault.Min, - max = RangeSliderDefault.Max, - step = RangeSliderDefault.Step, - value, - ...rest - } = this.props; - - const sharedProps = { - id: this.id, - min, - max, - step, - ...rest, - }; - - return isDualThumb(value) ? ( - - ) : ( - - ); - } +export function RangeSlider({ + min = RangeSliderDefault.Min, + max = RangeSliderDefault.Max, + step = RangeSliderDefault.Step, + value, + ...rest +}: Props) { + const id = useUniqueId('RangeSlider'); + + const sharedProps = { + id, + min, + max, + step, + ...rest, + }; + + return isDualThumb(value) ? ( + + ) : ( + + ); } function isDualThumb(value: RangeSliderValue): value is DualValue { return Array.isArray(value); } - -// Use named export once withAppProvider is refactored away -// eslint-disable-next-line import/no-default-export -export default withAppProvider()(RangeSlider); diff --git a/src/components/RangeSlider/index.ts b/src/components/RangeSlider/index.ts index edaf9b8d7df..1f4ce58bdb4 100644 --- a/src/components/RangeSlider/index.ts +++ b/src/components/RangeSlider/index.ts @@ -1,4 +1,2 @@ -import RangeSlider from './RangeSlider'; - -export {RangeSlider}; +export {RangeSlider} from './RangeSlider'; export {RangeSliderProps, RangeSliderValue, DualValue} from './types'; diff --git a/src/components/RangeSlider/tests/RangeSlider.test.tsx b/src/components/RangeSlider/tests/RangeSlider.test.tsx index 41c6a77a3c9..66a0c7be218 100644 --- a/src/components/RangeSlider/tests/RangeSlider.test.tsx +++ b/src/components/RangeSlider/tests/RangeSlider.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {mountWithAppProvider} from 'test-utilities/legacy'; -import RangeSlider from '../RangeSlider'; +import {RangeSlider} from '../RangeSlider'; import {DualThumb, SingleThumb} from '../components'; import {RangeSliderDefault} from '../utilities'; diff --git a/src/components/ResourceItem/ResourceItem.tsx b/src/components/ResourceItem/ResourceItem.tsx index c983a43c78f..e2422e14f71 100644 --- a/src/components/ResourceItem/ResourceItem.tsx +++ b/src/components/ResourceItem/ResourceItem.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, {useContext} from 'react'; import {HorizontalDotsMinor} from '@shopify/polaris-icons'; import {createUniqueIDFactory} from '@shopify/javascript-utilities/other'; import isEqual from 'lodash/isEqual'; import {classNames} from '../../utilities/css'; +import {useI18n} from '../../utilities/i18n'; import {DisableableAction} from '../../types'; import {ActionList} from '../ActionList'; import {Popover} from '../Popover'; @@ -12,10 +13,6 @@ import {ThumbnailProps} from '../Thumbnail'; import {ButtonGroup} from '../ButtonGroup'; import {Checkbox} from '../Checkbox'; import {Button, buttonsFrom} from '../Button'; -import { - withAppProvider, - WithAppProviderProps, -} from '../../utilities/with-app-provider'; import { ResourceListContext, @@ -30,11 +27,7 @@ export type MediaSize = 'small' | 'medium' | 'large'; export type MediaType = 'avatar' | 'thumbnail'; -interface WithContextTypes { - context: IJ; -} - -export interface Props { +export interface BaseProps { /** Visually hidden text for screen readers used for item link*/ accessibilityLabel?: string; /** Individual item name used by various text labels */ @@ -61,18 +54,23 @@ export interface Props { children?: React.ReactNode; } -export interface PropsWithUrl extends Props { +export interface PropsWithUrl extends BaseProps { url: string; onClick?(id?: string): void; } -export interface PropsWithClick extends Props { +export interface PropsWithClick extends BaseProps { url?: string; onClick(id?: string): void; } export type ResourceItemProps = PropsWithUrl | PropsWithClick; +interface PropsFromWrapper { + context: React.ContextType; + i18n: ReturnType; +} + interface State { actionsMenuVisible: boolean; focused: boolean; @@ -80,13 +78,7 @@ interface State { selected: boolean; } -export type CombinedProps = - | PropsWithUrl & - WithAppProviderProps & - WithContextTypes> - | PropsWithClick & - WithAppProviderProps & - WithContextTypes>; +export type CombinedProps = PropsFromWrapper & (PropsWithUrl | PropsWithClick); const getUniqueCheckboxID = createUniqueIDFactory('ResourceListItemCheckbox'); const getUniqueOverlayID = createUniqueIDFactory('ResourceListItemOverlay'); @@ -143,10 +135,10 @@ class BaseResourceItem extends React.Component { ariaControls, ariaExpanded, persistActions = false, - polaris: {intl}, accessibilityLabel, name, - context: {selectable, selectMode, loading}, + context: {selectable, selectMode, loading, resourceName}, + i18n, } = this.props; const {actionsMenuVisible, focused, focusedInner, selected} = this.state; @@ -162,7 +154,7 @@ class BaseResourceItem extends React.Component { if (selectable) { const checkboxAccessibilityLabel = - name || accessibilityLabel || intl.translate('Polaris.Common.checkbox'); + name || accessibilityLabel || i18n.translate('Polaris.Common.checkbox'); handleMarkup = (
{ ); const disclosureAccessibilityLabel = name - ? intl.translate('Polaris.ResourceList.Item.actionsDropdownLabel', { + ? i18n.translate('Polaris.ResourceList.Item.actionsDropdownLabel', { accessibilityLabel: name, }) - : intl.translate('Polaris.ResourceList.Item.actionsDropdown'); + : i18n.translate('Polaris.ResourceList.Item.actionsDropdown'); disclosureMarkup = (
@@ -276,10 +268,16 @@ class BaseResourceItem extends React.Component { const tabIndex = loading ? -1 : 0; + const ariaLabel = + accessibilityLabel || + i18n.translate('Polaris.ResourceList.Item.viewItem', { + itemName: name || (resourceName && resourceName.singular) || '', + }); + const accessibleMarkup = url ? ( { ) : ( - ); - - const className = classNames( - styles.SearchField, - (focused || active) && styles.focused, - ); - - return ( -
- - - - - - - - - {clearMarkup} -
-
- ); - } - - private handleFocus = () => { - const {onFocus} = this.props; - - if (onFocus) { - onFocus(); + if (!input.current) { + return; } - }; - private handleBlur = () => { - const {onBlur} = this.props; + input.current.value = ''; + onChange(''); + input.current.focus(); + }, [onCancel, onChange]); - if (onBlur) { - onBlur(); + useEffect(() => { + if (!input.current) { + return; } - }; - private handleClear = () => { - const {onCancel = noop, onChange} = this.props; - const { - input: {current: input}, - } = this; - - onCancel(); - - if (input != null) { - input.value = ''; - onChange(''); - input.focus(); + if (focused) { + input.current.focus(); + } else { + input.current.blur(); } - }; - - private handleChange = ({ - currentTarget, - }: React.ChangeEvent) => { - const {onChange} = this.props; - onChange(currentTarget.value); - }; + }, [focused]); + + const clearMarkup = value !== '' && ( + + ); + + const className = classNames( + styles.SearchField, + (focused || active) && styles.focused, + ); + + return ( +
+ + + + + + + + + {clearMarkup} +
+
+ ); } -function noop() {} - function preventDefault(event: React.KeyboardEvent) { if (event.key === 'Enter') { event.preventDefault(); } } - -// Use named export once withAppProvider is refactored away -// eslint-disable-next-line import/no-default-export -export default withAppProvider()(SearchField); diff --git a/src/components/TopBar/components/SearchField/index.ts b/src/components/TopBar/components/SearchField/index.ts index 5eff482b923..e1ba3495270 100644 --- a/src/components/TopBar/components/SearchField/index.ts +++ b/src/components/TopBar/components/SearchField/index.ts @@ -1,6 +1 @@ -// We've got to export a named SearchField otherwise other stuff breaks -// Fix once we stop exporting a default -// eslint-disable-next-line import/no-named-as-default -import SearchField, {SearchFieldProps} from './SearchField'; - -export {SearchField, SearchFieldProps}; +export {SearchField, SearchFieldProps} from './SearchField'; diff --git a/src/components/TopBar/components/SearchField/tests/SearchField.test.tsx b/src/components/TopBar/components/SearchField/tests/SearchField.test.tsx index c9b2316a625..2ccd5bf62ec 100644 --- a/src/components/TopBar/components/SearchField/tests/SearchField.test.tsx +++ b/src/components/TopBar/components/SearchField/tests/SearchField.test.tsx @@ -2,10 +2,7 @@ import React from 'react'; import {CircleCancelMinor} from '@shopify/polaris-icons'; import {ReactWrapper} from 'enzyme'; import {mountWithAppProvider} from 'test-utilities/legacy'; -// We've got to export a named SearchField otherwise other stuff breaks -// Fix once we stop exporting a default -// eslint-disable-next-line import/no-named-as-default -import SearchField from '../SearchField'; +import {SearchField} from '../SearchField'; describe('', () => { it('mounts', () => { diff --git a/tests/pa11y-utilities.test.js b/tests/pa11y-utilities.test.js deleted file mode 100644 index 171cd25d967..00000000000 --- a/tests/pa11y-utilities.test.js +++ /dev/null @@ -1,76 +0,0 @@ -const shitlistCheck = require('./../scripts/pa11y-utilities.js').shitlistCheck; - -describe('shitlistCheck', () => { - const shitlist = { - 伊藤美来: [ - { - code: 'ショッキングブルー', - }, - ], - 豊田萌絵: [ - { - code: 'Pyxis', - }, - ], - }; - - it('filters errors on the shitlist', () => { - const issueList = [ - { - exampleID: '伊藤美来', - issues: [{code: 'ショッキングブルー'}, {code: '風の戦士'}], - }, - ]; - - const {results} = shitlistCheck(issueList, shitlist); - - expect( - results[0].issues.findIndex(({code}) => code === 'ショッキングブルー'), - ).toBe(-1); - expect(results[0].issues.findIndex(({code}) => code === '風の戦士')).toBe( - 0, - ); - expect(results[0].issues).toHaveLength(1); - }); - - it('leaves errors not on the shitlist', () => { - const issueList = [ - { - exampleID: '伊藤美来', - issues: [{code: '美来の色探し'}, {code: '風の戦士'}], - }, - ]; - - const {results} = shitlistCheck(issueList, shitlist); - - expect(results[0].issues).toHaveLength(2); - }); - - it('removes entries with no issues from the list', () => { - const issueList = [ - { - exampleID: '虹香', - issues: [], - }, - ]; - - const {results} = shitlistCheck(issueList, shitlist); - - expect(results).toHaveLength(0); - }); - - it('returns a list with errors that werent found', () => { - const issueList = [ - { - exampleID: '伊藤美来', - issues: [{code: 'ショッキングブルー'}], - }, - ]; - - const {remainingIssues} = shitlistCheck(issueList, shitlist); - - expect(remainingIssues).toHaveLength(1); - expect(remainingIssues[0].issues).toHaveLength(1); - expect(remainingIssues[0].issues[0].code).toBe('Pyxis'); - }); -});