From cc02259ddb8faeed27813ddce850b5653fe1d0d3 Mon Sep 17 00:00:00 2001 From: langemike Date: Wed, 24 Jan 2024 13:15:23 +0100 Subject: [PATCH] feat(a11y): many accessibility optimisations fix(a11y): prevent double ids on inputs by requiring a name feat(a11y): apply aria-modal attribute and move header landmark (#48) feat(a11y): update button role and html structure of account and player pages (#47) feat(a11y): add correct text markups and aria attributes (#46) feat(home): add (geo) error message when all playlists are empty feat(a11y): add form error announcement feat(a11y): add solid header background color to ensure accessibility feat(a11y): implement aria-invalid and aria-described by to inputs on error feat(project): add google fonts from env vars feat: keyboard accessible LayoutGrid feat: optimize featured shelf slider for accessibility feat(a11y): accessible sidebar &
landmark feat(a11y): enhance dialog and modals accessibility fix(a11y): alt text for images for EPG fix(a11y): empty alt for image because of adjacent text alternative fix(a11y): fix arrow keys for offer radio buttons fix(a11y): skiplink first element feat(a11y): improve html structure for VideoListItem fix(e2e): cardgrid card navigation feat(a11y): apply lang attribute to custom fields feat(a11y): accessible focus outline --- packages/common/src/constants.ts | 2 +- packages/common/src/env.ts | 10 + packages/common/src/utils/common.ts | 24 + packages/hooks-react/src/usePlaylists.ts | 73 +++ packages/testing/fixtures/favorites.json | 417 ++++++++++++++ .../src/components/Account/Account.tsx | 38 +- .../__snapshots__/Account.test.tsx.snap | 52 +- .../src/components/Button/Button.module.scss | 16 +- .../ui-react/src/components/Button/Button.tsx | 2 +- .../Button/__snapshots__/Button.test.tsx.snap | 1 + .../CancelSubscriptionForm.tsx | 1 + .../CancelSubscriptionForm.test.tsx.snap | 2 + .../src/components/Card/Card.module.scss | 7 +- .../ui-react/src/components/Card/Card.tsx | 23 +- .../src/components/CardGrid/CardGrid.tsx | 40 +- .../__snapshots__/CardGrid.test.tsx.snap | 36 +- .../src/components/Checkbox/Checkbox.tsx | 20 +- .../__snapshots__/Checkbox.test.tsx.snap | 1 + .../CheckoutForm/CheckoutForm.module.scss | 13 +- .../components/CheckoutForm/CheckoutForm.tsx | 2 +- .../__snapshots__/CheckoutForm.test.tsx.snap | 47 +- .../ChooseOfferForm.module.scss | 15 +- .../ChooseOfferForm/ChooseOfferForm.test.tsx | 16 +- .../ChooseOfferForm/ChooseOfferForm.tsx | 96 ++-- .../ChooseOfferForm.test.tsx.snap | 9 +- .../CollapsibleText/CollapsibleText.tsx | 11 +- .../CollapsibleText.test.tsx.snap | 4 +- .../ConfirmationDialog/ConfirmationDialog.tsx | 6 +- .../ConfirmationForm.test.tsx.snap | 1 + .../CreditCardCVCField.test.tsx.snap | 3 + .../CreditCardExpiryField.test.tsx.snap | 3 + .../CreditCardNumberField.test.tsx.snap | 3 + .../CustomRegisterField.tsx | 20 +- .../CustomRegisterField.test.tsx.snap | 20 +- .../src/components/DateField/DateField.tsx | 15 +- .../DeleteAccountModal/DeleteAccountModal.tsx | 21 +- .../DevConfigSelector/DevConfigSelector.tsx | 1 + .../ui-react/src/components/Dialog/Dialog.tsx | 15 +- .../Dialog/__snapshots__/Dialog.test.tsx.snap | 6 +- .../DialogBackButton/DialogBackButton.tsx | 5 +- .../DialogBackButton.test.tsx.snap | 2 +- .../src/components/Dropdown/Dropdown.tsx | 20 +- .../EditCardPaymentForm.tsx | 12 +- .../EditPasswordForm.test.tsx.snap | 12 + packages/ui-react/src/components/Epg/Epg.tsx | 8 +- .../components/EpgChannel/EpgChannelItem.tsx | 6 +- .../EpgProgramItem/EpgProgramItem.tsx | 3 +- .../src/components/ErrorPage/ErrorPage.tsx | 38 +- .../__snapshots__/ErrorPage.test.tsx.snap | 16 +- .../components/Favorites/Favorites.test.tsx | 17 +- .../src/components/Favorites/Favorites.tsx | 15 +- .../__snapshots__/Favorites.test.tsx.snap | 177 +++++- .../Filter/__snapshots__/Filter.test.tsx.snap | 4 + .../FinalizePayment/FinalizePayment.tsx | 3 + .../ForgotPasswordForm.test.tsx.snap | 3 + .../ui-react/src/components/Form/Form.tsx | 1 + .../src/components/Form/FormSection.tsx | 1 - .../FormFeedback/FormFeedback.module.scss | 12 + .../components/FormFeedback/FormFeedback.tsx | 20 +- .../__snapshots__/FormFeedback.test.tsx.snap | 1 + .../src/components/Header/Header.test.tsx | 1 + .../ui-react/src/components/Header/Header.tsx | 22 +- .../Header/__snapshots__/Header.test.tsx.snap | 18 +- .../src/components/HelperText/HelperText.tsx | 12 +- .../__snapshots__/HelperText.test.tsx.snap | 2 + .../IconButton/IconButton.module.scss | 1 + .../LayoutGrid/LayoutGrid.module.scss | 8 + .../src/components/LayoutGrid/LayoutGrid.tsx | 153 +++++ .../src/components/LoginForm/LoginForm.tsx | 2 +- .../__snapshots__/LoginForm.test.tsx.snap | 6 + .../MarkdownComponent/MarkdownComponent.tsx | 5 +- .../src/components/MenuButton/MenuButton.tsx | 3 +- .../__snapshots__/MenuButton.test.tsx.snap | 1 - .../src/components/Modal/Modal.test.tsx | 5 +- .../ui-react/src/components/Modal/Modal.tsx | 25 +- .../NoPaymentRequired.test.tsx.snap | 1 + .../__snapshots__/PasswordField.test.tsx.snap | 6 + .../PayPal/__snapshots__/PayPal.test.tsx.snap | 1 + .../src/components/Payment/Payment.tsx | 17 +- .../__snapshots__/Payment.test.tsx.snap | 51 +- .../__snapshots__/PaymentFailed.test.tsx.snap | 1 + .../PaymentMethodForm.module.scss | 13 +- .../PaymentMethodForm/PaymentMethodForm.tsx | 8 +- .../PersonalDetailsForm.test.tsx.snap | 113 +++- .../components/Popover/Popover.module.scss | 2 +- .../__snapshots__/Popover.test.tsx.snap | 1 - .../src/components/Radio/Radio.module.scss | 5 +- .../ui-react/src/components/Radio/Radio.tsx | 25 +- .../Radio/__snapshots__/Radio.test.tsx.snap | 13 +- .../RegistrationForm/RegistrationForm.tsx | 49 +- .../RegistrationForm.test.tsx.snap | 8 + .../RenewSubscriptionForm.tsx | 1 + .../RenewSubscriptionForm.test.tsx.snap | 2 + .../ResetPasswordForm/ResetPasswordForm.tsx | 2 +- .../ResetPasswordForm.test.tsx.snap | 2 + .../__snapshots__/ShareButton.test.tsx.snap | 1 + .../src/components/Shelf/Shelf.module.scss | 4 +- .../ui-react/src/components/Shelf/Shelf.tsx | 8 +- .../Shelf/__snapshots__/Shelf.test.tsx.snap | 540 +++++++++--------- .../components/Sidebar/Sidebar.module.scss | 3 + .../src/components/Sidebar/Sidebar.test.tsx | 12 +- .../src/components/Sidebar/Sidebar.tsx | 19 +- .../__snapshots__/Sidebar.test.tsx.snap | 55 +- .../SubscriptionCancelled.test.tsx.snap | 1 + .../SubscriptionRenewed.test.tsx.snap | 1 + .../TextField/TextField.module.scss | 6 + .../components/TextField/TextField.test.tsx | 18 +- .../src/components/TextField/TextField.tsx | 53 +- .../__snapshots__/TextField.test.tsx.snap | 73 +++ .../src/components/TileDock/TileDock.tsx | 8 +- .../UpgradeSubscription.tsx | 10 +- .../__snapshots__/UserMenu.test.tsx.snap | 3 - .../VideoDetails/VideoDetails.module.scss | 5 +- .../VideoDetails/VideoDetails.test.tsx | 3 +- .../components/VideoDetails/VideoDetails.tsx | 7 +- .../__snapshots__/VideoDetails.test.tsx.snap | 10 +- .../VideoLayout/VideoLayout.module.scss | 6 +- .../components/VideoLayout/VideoLayout.tsx | 3 +- .../VideoList/VideoList.module.scss | 13 +- .../src/components/VideoList/VideoList.tsx | 32 +- .../VideoListItem/VideoListItem.module.scss | 36 +- .../VideoListItem/VideoListItem.tsx | 41 +- .../WaitingForPayment/WaitingForPayment.tsx | 9 +- .../__snapshots__/Welcome.test.tsx.snap | 1 + .../AccountModal/forms/CancelSubscription.tsx | 13 +- .../AccountModal/forms/Checkout.tsx | 10 +- .../AccountModal/forms/ChooseOffer.tsx | 1 - .../AccountModal/forms/EditPassword.tsx | 6 +- .../containers/AccountModal/forms/Login.tsx | 8 +- .../AccountModal/forms/Registration.tsx | 14 +- .../AccountModal/forms/RenewSubscription.tsx | 3 + .../AccountModal/forms/ResetPassword.tsx | 4 +- .../AdyenInitialPayment.tsx | 14 +- .../AdyenPaymentDetails.tsx | 8 +- .../AnnoucementProvider.tsx | 45 ++ .../src/containers/Layout/Layout.module.scss | 4 +- .../ui-react/src/containers/Layout/Layout.tsx | 42 +- .../Layout/__snapshots__/Layout.test.tsx.snap | 113 ++-- .../PlaylistContainer/PlaylistContainer.tsx | 57 -- .../ShelfList/ShelfList.module.scss | 8 - .../src/containers/ShelfList/ShelfList.tsx | 102 ++-- .../containers/TrailerModal/TrailerModal.tsx | 8 +- .../ui-react/src/pages/Home/Home.test.tsx | 10 +- .../Home/__snapshots__/Home.test.tsx.snap | 248 ++++---- .../ScreenRouting/PlaylistScreenRouter.tsx | 4 + .../PlaylistGrid/PlaylistGrid.tsx | 13 +- .../PlaylistLiveChannels.tsx | 2 +- .../src/pages/Search/Search.module.scss | 6 +- packages/ui-react/src/pages/Search/Search.tsx | 4 +- packages/ui-react/src/pages/User/User.tsx | 27 +- .../User/__snapshots__/User.test.tsx.snap | 154 +++-- .../ui-react/src/styles/accessibility.scss | 32 ++ packages/ui-react/src/utils/theming.ts | 25 +- packages/ui-react/test/utils.tsx | 5 +- platforms/web/public/locales/en/account.json | 21 +- platforms/web/public/locales/en/common.json | 2 +- platforms/web/public/locales/en/error.json | 4 +- platforms/web/public/locales/en/user.json | 6 +- platforms/web/public/locales/en/video.json | 1 + platforms/web/public/locales/es/account.json | 21 +- platforms/web/public/locales/es/common.json | 2 +- platforms/web/public/locales/es/error.json | 4 +- platforms/web/public/locales/es/user.json | 6 +- platforms/web/public/locales/es/video.json | 1 + .../web/scripts/build-tools/buildTools.ts | 148 +++++ platforms/web/scripts/build-tools/settings.ts | 17 - platforms/web/src/App.tsx | 5 +- .../DemoConfigDialog.test.tsx.snap | 23 +- platforms/web/src/index.tsx | 2 + platforms/web/src/styles/main.scss | 4 + platforms/web/test-e2e/tests/account_test.ts | 14 +- .../web/test-e2e/tests/live_channel_test.ts | 4 +- .../tests/payments/subscription_test.ts | 6 +- .../web/test-e2e/tests/video_detail_test.ts | 2 +- platforms/web/test-e2e/utils/steps_file.ts | 17 +- platforms/web/vite.config.ts | 94 +-- 176 files changed, 3128 insertions(+), 1300 deletions(-) create mode 100644 packages/hooks-react/src/usePlaylists.ts create mode 100644 packages/testing/fixtures/favorites.json create mode 100644 packages/ui-react/src/components/LayoutGrid/LayoutGrid.module.scss create mode 100644 packages/ui-react/src/components/LayoutGrid/LayoutGrid.tsx create mode 100644 packages/ui-react/src/containers/AnnouncementProvider/AnnoucementProvider.tsx delete mode 100644 packages/ui-react/src/containers/PlaylistContainer/PlaylistContainer.tsx create mode 100644 platforms/web/scripts/build-tools/buildTools.ts delete mode 100644 platforms/web/scripts/build-tools/settings.ts diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 08b199752..06c8c0ee1 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -60,7 +60,7 @@ export const CACHE_TIME = 60 * 1000 * 20; // 20 minutes export const STALE_TIME = 60 * 1000 * 20; -export const CARD_ASPECT_RATIOS = ['2:1', '16:9', '5:3', '4:3', '1:1', '9:13', '2:3', '9:16'] as const; +export const CARD_ASPECT_RATIOS = ['1:1', '2:1', '2:3', '4:3', '5:3', '16:9', '9:13', '9:16'] as const; export const DEFAULT_FEATURES = { canUpdateEmail: false, diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index 5d4066571..50c854c56 100644 --- a/packages/common/src/env.ts +++ b/packages/common/src/env.ts @@ -3,8 +3,13 @@ export type Env = { APP_API_BASE_URL: string; APP_PLAYER_ID: string; APP_FOOTER_TEXT: string; + APP_DEFAULT_LANGUAGE: string; + APP_DEFAULT_CONFIG_SOURCE?: string; APP_PLAYER_LICENSE_KEY?: string; + + APP_BODY_FONT?: string; + APP_BODY_ALT_FONT?: string; }; const env: Env = { @@ -12,6 +17,7 @@ const env: Env = { APP_API_BASE_URL: 'https://cdn.jwplayer.com', APP_PLAYER_ID: 'M4qoGvUk', APP_FOOTER_TEXT: '', + APP_DEFAULT_LANGUAGE: 'en', }; export const configureEnv = (options: Partial) => { @@ -19,9 +25,13 @@ export const configureEnv = (options: Partial) => { env.APP_API_BASE_URL = options.APP_API_BASE_URL || env.APP_API_BASE_URL; env.APP_PLAYER_ID = options.APP_PLAYER_ID || env.APP_PLAYER_ID; env.APP_FOOTER_TEXT = options.APP_FOOTER_TEXT || env.APP_FOOTER_TEXT; + env.APP_DEFAULT_LANGUAGE = options.APP_DEFAULT_LANGUAGE || env.APP_DEFAULT_LANGUAGE; env.APP_DEFAULT_CONFIG_SOURCE ||= options.APP_DEFAULT_CONFIG_SOURCE; env.APP_PLAYER_LICENSE_KEY ||= options.APP_PLAYER_LICENSE_KEY; + + env.APP_BODY_FONT = options.APP_BODY_FONT || env.APP_BODY_FONT; + env.APP_BODY_ALT_FONT = options.APP_BODY_ALT_FONT || env.APP_BODY_ALT_FONT; }; export default env; diff --git a/packages/common/src/utils/common.ts b/packages/common/src/utils/common.ts index 5b3395186..f3666cb2c 100644 --- a/packages/common/src/utils/common.ts +++ b/packages/common/src/utils/common.ts @@ -5,6 +5,30 @@ export function debounce void>(callback: T, wait = timeout = setTimeout(() => callback(...args), wait); }; } +export function throttle unknown>(func: T, limit: number): (...args: Parameters) => void { + let lastFunc: NodeJS.Timeout | undefined; + let lastRan: number | undefined; + + return function (this: ThisParameterType, ...args: Parameters): void { + const timeSinceLastRan = lastRan ? Date.now() - lastRan : limit; + + if (timeSinceLastRan >= limit) { + func.apply(this, args); + lastRan = Date.now(); + } else if (!lastFunc) { + lastFunc = setTimeout(() => { + if (lastRan) { + const timeSinceLastRan = Date.now() - lastRan; + if (timeSinceLastRan >= limit) { + func.apply(this, args); + lastRan = Date.now(); + } + } + lastFunc = undefined; + }, limit - timeSinceLastRan); + } + }; +} export const unicodeToChar = (text: string) => { return text.replace(/\\u[\dA-F]{4}/gi, (match) => { diff --git a/packages/hooks-react/src/usePlaylists.ts b/packages/hooks-react/src/usePlaylists.ts new file mode 100644 index 000000000..c430e816f --- /dev/null +++ b/packages/hooks-react/src/usePlaylists.ts @@ -0,0 +1,73 @@ +import { PersonalShelf, PersonalShelves, PLAYLIST_LIMIT } from '@jwp/ott-common/src/constants'; +import ApiService from '@jwp/ott-common/src/services/ApiService'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import { useFavoritesStore } from '@jwp/ott-common/src/stores/FavoritesStore'; +import { useWatchHistoryStore } from '@jwp/ott-common/src/stores/WatchHistoryStore'; +import { generatePlaylistPlaceholder } from '@jwp/ott-common/src/utils/collection'; +import { isTruthyCustomParamValue } from '@jwp/ott-common/src/utils/common'; +import { isScheduledOrLiveMedia } from '@jwp/ott-common/src/utils/liveEvent'; +import type { Content } from '@jwp/ott-common/types/config'; +import type { Playlist } from '@jwp/ott-common/types/playlist'; +import { useQueries, useQueryClient } from 'react-query'; + +const placeholderData = generatePlaylistPlaceholder(30); + +type UsePlaylistResult = { + data: Playlist | undefined; + isLoading: boolean; + isSuccess?: boolean; + error?: unknown; +}[]; + +const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undefined) => { + const page_limit = PLAYLIST_LIMIT.toString(); + const queryClient = useQueryClient(); + const apiService = getModule(ApiService); + + const favorites = useFavoritesStore((state) => state.getPlaylist()); + const watchHistory = useWatchHistoryStore((state) => state.getPlaylist()); + + const playlistQueries = useQueries( + content.map(({ contentId, type }, index) => ({ + enabled: !!contentId && (!rowsToLoad || rowsToLoad > index) && !PersonalShelves.some((pType) => pType === type), + queryKey: ['playlist', contentId], + queryFn: async () => { + const playlist = await apiService.getPlaylistById(contentId, { page_limit }); + + // This pre-caches all playlist items and makes navigating a lot faster. + playlist?.playlist?.forEach((playlistItem) => { + queryClient.setQueryData(['media', playlistItem.mediaid], playlistItem); + }); + + return playlist; + }, + placeholderData: !!contentId && placeholderData, + refetchInterval: (data: Playlist | undefined) => { + if (!data) return false; + + const autoRefetch = isTruthyCustomParamValue(data.refetch) || data.playlist.some(isScheduledOrLiveMedia); + + return autoRefetch ? 1000 * 30 : false; + }, + retry: false, + })), + ); + + const result: UsePlaylistResult = content.map(({ type }, index) => { + if (type === PersonalShelf.Favorites) return { data: favorites, isLoading: false, isSuccess: true }; + if (type === PersonalShelf.ContinueWatching) return { data: watchHistory, isLoading: false, isSuccess: true }; + + const { data, isLoading, isSuccess, error } = playlistQueries[index]; + + return { + data, + isLoading, + isSuccess, + error, + }; + }); + + return result; +}; + +export default usePlaylists; diff --git a/packages/testing/fixtures/favorites.json b/packages/testing/fixtures/favorites.json new file mode 100644 index 000000000..ab2c6a930 --- /dev/null +++ b/packages/testing/fixtures/favorites.json @@ -0,0 +1,417 @@ +{ + "feedid": "KKOhckQL", + "title": "Favorites", + "playlist": [ + { + "title": "SVOD 002: Caminandes 1 llama drama", + "mediaid": "1TJAvj2S", + "link": "https://cdn.jwplayer.com/previews/1TJAvj2S", + "image": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=720", + "images": [ + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=320", + "width": 320, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=480", + "width": 480, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=640", + "width": 640, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=720", + "width": 720, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=1280", + "width": 1280, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=1920", + "width": 1920, + "type": "image/jpeg" + } + ], + "feedid": "bdH6HTUi", + "duration": 90, + "pubdate": 1703166863, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla ligula, sollicitudin id felis eu, consequat aliquam ante. Suspendisse lacinia felis a quam laoreet, non tristique quam efficitur. Morbi ultrices, nibh et fringilla aliquet, ligula tortor porta libero, ut elementum nibh nisl vel odio. Praesent ornare luctus arcu nec condimentum. Vestibulum fringilla egestas neque, feugiat sollicitudin eros aliquet non. In enim augue, sodales eget dignissim eget, sagittis sit amet eros. Nulla tristique nisi iaculis dui egestas mollis. Aenean libero odio, vestibulum quis sodales pulvinar, consequat ut nisl.", + "tags": "svod", + "recommendations": "https://cdn.jwplayer.com/v2/playlists/bdH6HTUi?related_media_id=1TJAvj2S", + "sources": [ + { + "file": "https://cdn.jwplayer.com/manifests/1TJAvj2S.m3u8", + "type": "application/vnd.apple.mpegurl" + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-xAdPQ1TN.m4a", + "type": "audio/mp4", + "label": "AAC Audio", + "bitrate": 113513, + "filesize": 1277026 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-1Yfbe0HO.mp4", + "type": "video/mp4", + "height": 180, + "width": 320, + "label": "180p", + "bitrate": 241872, + "filesize": 2721071, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-JjxC7ylo.mp4", + "type": "video/mp4", + "height": 270, + "width": 480, + "label": "270p", + "bitrate": 356443, + "filesize": 4009992, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-kqEB96Md.mp4", + "type": "video/mp4", + "height": 360, + "width": 640, + "label": "360p", + "bitrate": 401068, + "filesize": 4512018, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-MskLmv79.mp4", + "type": "video/mp4", + "height": 406, + "width": 720, + "label": "406p", + "bitrate": 466271, + "filesize": 5245549, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-MCyoQl96.mp4", + "type": "video/mp4", + "height": 540, + "width": 960, + "label": "540p", + "bitrate": 713837, + "filesize": 8030667, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-yYzmiSbt.mp4", + "type": "video/mp4", + "height": 720, + "width": 1280, + "label": "720p", + "bitrate": 1088928, + "filesize": 12250450, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-H4t30RCN.mp4", + "type": "video/mp4", + "height": 1080, + "width": 1920, + "label": "1080p", + "bitrate": 2391552, + "filesize": 26904961, + "framerate": 24 + } + ], + "tracks": [ + { + "file": "https://cdn.jwplayer.com/strips/1TJAvj2S-120.vtt", + "kind": "thumbnails" + } + ], + "variations": {}, + "cardImage": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/images/card.webp?poster_fallback=1", + "channelLogoImage": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/images/channel_logo.webp?poster_fallback=1", + "backgroundImage": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/images/background.webp?poster_fallback=1" + }, + { + "title": "SVOD 003: Caminandes 2 gran dillama", + "mediaid": "rnibIt0n", + "link": "https://cdn.jwplayer.com/previews/rnibIt0n", + "image": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=720", + "images": [ + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=320", + "width": 320, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=480", + "width": 480, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=640", + "width": 640, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=720", + "width": 720, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=1280", + "width": 1280, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=1920", + "width": 1920, + "type": "image/jpeg" + } + ], + "feedid": "bdH6HTUi", + "duration": 146, + "pubdate": 1703166863, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla ligula, sollicitudin id felis eu, consequat aliquam ante. Suspendisse lacinia felis a quam laoreet, non tristique quam efficitur. Morbi ultrices, nibh et fringilla aliquet, ligula tortor porta libero, ut elementum nibh nisl vel odio. Praesent ornare luctus arcu nec condimentum. Vestibulum fringilla egestas neque, feugiat sollicitudin eros aliquet non. In enim augue, sodales eget dignissim eget, sagittis sit amet eros. Nulla tristique nisi iaculis dui egestas mollis. Aenean libero odio, vestibulum quis sodales pulvinar, consequat ut nisl.", + "tags": "svod", + "recommendations": "https://cdn.jwplayer.com/v2/playlists/bdH6HTUi?related_media_id=rnibIt0n", + "sources": [ + { + "file": "https://cdn.jwplayer.com/manifests/rnibIt0n.m3u8", + "type": "application/vnd.apple.mpegurl" + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-xAdPQ1TN.m4a", + "type": "audio/mp4", + "label": "AAC Audio", + "bitrate": 113503, + "filesize": 2071433 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-1Yfbe0HO.mp4", + "type": "video/mp4", + "height": 180, + "width": 320, + "label": "180p", + "bitrate": 342175, + "filesize": 6244705, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-JjxC7ylo.mp4", + "type": "video/mp4", + "height": 270, + "width": 480, + "label": "270p", + "bitrate": 501738, + "filesize": 9156729, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-kqEB96Md.mp4", + "type": "video/mp4", + "height": 360, + "width": 640, + "label": "360p", + "bitrate": 579321, + "filesize": 10572609, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-MskLmv79.mp4", + "type": "video/mp4", + "height": 406, + "width": 720, + "label": "406p", + "bitrate": 673083, + "filesize": 12283769, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-MCyoQl96.mp4", + "type": "video/mp4", + "height": 540, + "width": 960, + "label": "540p", + "bitrate": 984717, + "filesize": 17971095, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-yYzmiSbt.mp4", + "type": "video/mp4", + "height": 720, + "width": 1280, + "label": "720p", + "bitrate": 1527270, + "filesize": 27872694, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-H4t30RCN.mp4", + "type": "video/mp4", + "height": 1080, + "width": 1920, + "label": "1080p", + "bitrate": 3309652, + "filesize": 60401155, + "framerate": 24 + } + ], + "tracks": [ + { + "file": "https://cdn.jwplayer.com/strips/rnibIt0n-120.vtt", + "kind": "thumbnails" + } + ], + "variations": {}, + "genre": "Animation", + "cardImage": "https://cdn.jwplayer.com/v2/media/rnibIt0n/images/card.webp?poster_fallback=1", + "channelLogoImage": "https://cdn.jwplayer.com/v2/media/rnibIt0n/images/channel_logo.webp?poster_fallback=1", + "backgroundImage": "https://cdn.jwplayer.com/v2/media/rnibIt0n/images/background.webp?poster_fallback=1" + }, + { + "title": "SVOD 001: Tears of Steel", + "mediaid": "MaCvdQdE", + "link": "https://cdn.jwplayer.com/previews/MaCvdQdE", + "image": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=720", + "images": [ + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=320", + "width": 320, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=480", + "width": 480, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=640", + "width": 640, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=720", + "width": 720, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=1280", + "width": 1280, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=1920", + "width": 1920, + "type": "image/jpeg" + } + ], + "feedid": "E2uaFiUM", + "duration": 734, + "pubdate": 1703166863, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla ligula, sollicitudin id felis eu, consequat aliquam ante. Suspendisse lacinia felis a quam laoreet, non tristique quam efficitur. Morbi ultrices, nibh et fringilla aliquet, ligula tortor porta libero, ut elementum nibh nisl vel odio. Praesent ornare luctus arcu nec condimentum. Vestibulum fringilla egestas neque, feugiat sollicitudin eros aliquet non. In enim augue, sodales eget dignissim eget, sagittis sit amet eros. Nulla tristique nisi iaculis dui egestas mollis. Aenean libero odio, vestibulum quis sodales pulvinar, consequat ut nisl.", + "tags": "svod", + "sources": [ + { + "file": "https://cdn.jwplayer.com/manifests/MaCvdQdE.m3u8", + "type": "application/vnd.apple.mpegurl" + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-xAdPQ1TN.m4a", + "type": "audio/mp4", + "label": "AAC Audio", + "bitrate": 113413, + "filesize": 10405724 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-1Yfbe0HO.mp4", + "type": "video/mp4", + "height": 134, + "width": 320, + "label": "180p", + "bitrate": 388986, + "filesize": 35689542, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-JjxC7ylo.mp4", + "type": "video/mp4", + "height": 200, + "width": 480, + "label": "270p", + "bitrate": 575378, + "filesize": 52790944, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-kqEB96Md.mp4", + "type": "video/mp4", + "height": 266, + "width": 640, + "label": "360p", + "bitrate": 617338, + "filesize": 56640812, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-MskLmv79.mp4", + "type": "video/mp4", + "height": 300, + "width": 720, + "label": "406p", + "bitrate": 715724, + "filesize": 65667691, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-MCyoQl96.mp4", + "type": "video/mp4", + "height": 400, + "width": 960, + "label": "540p", + "bitrate": 1029707, + "filesize": 94475629, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-yYzmiSbt.mp4", + "type": "video/mp4", + "height": 534, + "width": 1280, + "label": "720p", + "bitrate": 1570612, + "filesize": 144103685, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-H4t30RCN.mp4", + "type": "video/mp4", + "height": 800, + "width": 1920, + "label": "1080p", + "bitrate": 3081227, + "filesize": 282702650, + "framerate": 24 + } + ], + "tracks": [ + { + "file": "https://cdn.jwplayer.com/strips/MaCvdQdE-120.vtt", + "kind": "thumbnails" + } + ], + "variations": {}, + "cardImage": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/images/card.webp?poster_fallback=1", + "channelLogoImage": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/images/channel_logo.webp?poster_fallback=1", + "backgroundImage": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/images/background.webp?poster_fallback=1" + } + ] +} diff --git a/packages/ui-react/src/components/Account/Account.tsx b/packages/ui-react/src/components/Account/Account.tsx index ed715b695..71c4c2c2f 100644 --- a/packages/ui-react/src/components/Account/Account.tsx +++ b/packages/ui-react/src/components/Account/Account.tsx @@ -13,6 +13,7 @@ import { formatConsents, formatConsentsFromValues, formatConsentsToRegisterField import useToggle from '@jwp/ott-hooks-react/src/useToggle'; import Visibility from '@jwp/ott-theme/assets/icons/visibility.svg?react'; import VisibilityOff from '@jwp/ott-theme/assets/icons/visibility_off.svg?react'; +import env from '@jwp/ott-common/src/env'; import type { FormSectionContentArgs, FormSectionProps } from '../Form/FormSection'; import Alert from '../Alert/Alert'; @@ -25,6 +26,7 @@ import HelperText from '../HelperText/HelperText'; import CustomRegisterField from '../CustomRegisterField/CustomRegisterField'; import Icon from '../Icon/Icon'; import { modalURLFromLocation } from '../../utils/location'; +import { useAriaAnnouncer } from '../../containers/AnnouncementProvider/AnnoucementProvider'; import styles from './Account.module.scss'; @@ -45,13 +47,15 @@ interface FormErrors { const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true }: Props): JSX.Element => { const accountController = getModule(AccountController); - const { t } = useTranslation('user'); + const { t, i18n } = useTranslation('user'); + const announce = useAriaAnnouncer(); const navigate = useNavigate(); const location = useLocation(); const [viewPassword, toggleViewPassword] = useToggle(); const exportData = useMutation(accountController.exportAccountData); const [isAlertVisible, setIsAlertVisible] = useState(false); const exportDataMessage = exportData.isSuccess ? t('account.export_data_success') : t('account.export_data_error'); + const htmlLang = i18n.language !== env.APP_DEFAULT_LANGUAGE ? env.APP_DEFAULT_LANGUAGE : undefined; useEffect(() => { if (exportData.isSuccess || exportData.isError) { @@ -203,15 +207,17 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } return ( <> +

{t('nav.account')}

+
{[ formSection({ label: t('account.about_you'), editButton: t('account.edit_information'), - onSubmit: (values) => { + onSubmit: async (values) => { const consents = formatConsentsFromValues(publisherConsents, { ...values.metadata, ...values.consentsValues }); - return accountController.updateUser({ + const response = await accountController.updateUser({ firstName: values.firstName || '', lastName: values.lastName || '', metadata: { @@ -220,6 +226,10 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } consents: JSON.stringify(consents), }, }); + + announce(t('account.update_success', { section: t('account.about_you') }), 'success'); + + return response; }, content: (section) => ( <> @@ -233,6 +243,7 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } helperText={section.errors?.firstName} disabled={section.isBusy} editing={section.isEditing} + lang={htmlLang} /> ), }), formSection({ label: t('account.email'), - onSubmit: (values) => - accountController.updateUser({ + onSubmit: async (values) => { + const response = await accountController.updateUser({ email: values.email || '', confirmationPassword: values.confirmationPassword, - }), + }); + + announce(t('account.update_success', { section: t('account.email') }), 'success'); + + return response; + }, canSave: (values) => !!(values.email && values.confirmationPassword), editButton: t('account.edit_account'), readOnly: !canUpdateEmail, @@ -304,7 +321,13 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } formSection({ label: t('account.terms_and_tracking'), saveButton: t('account.update_consents'), - onSubmit: (values) => accountController.updateConsents(formatConsentsFromValues(publisherConsents, values.consentsValues)), + onSubmit: async (values) => { + const response = await accountController.updateConsents(formatConsentsFromValues(publisherConsents, values.consentsValues)); + + announce(t('account.update_success', { section: t('account.terms_and_tracking') }), 'success'); + + return response; + }, content: (section) => ( <> {termsConsents?.map((consent, index) => ( @@ -315,6 +338,7 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } onChange={section.onChange} label={formatConsentLabel(consent.label)} disabled={consent.required || section.isBusy} + lang={htmlLang} /> ))} diff --git a/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap b/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap index 175e6fe0c..6578f12f2 100644 --- a/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap +++ b/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap @@ -2,6 +2,11 @@ exports[` > renders and matches snapshot 1`] = `
+

+ nav.account +

> renders and matches snapshot 1`] = ` > account.firstname -

- John -

+
> renders and matches snapshot 1`] = ` > account.lastname -

- Doe -

+
> renders and matches snapshot 1`] = ` > account.email -

- email@domain.com -

+
> renders and matches snapshot 1`] = ` class="_row_531f07" > > renders and matches snapshot 1`] = ` /> diff --git a/packages/ui-react/src/components/Button/Button.module.scss b/packages/ui-react/src/components/Button/Button.module.scss index 429a2530e..40577575a 100644 --- a/packages/ui-react/src/components/Button/Button.module.scss +++ b/packages/ui-react/src/components/Button/Button.module.scss @@ -1,5 +1,6 @@ @use '@jwp/ott-ui-react/src/styles/variables'; @use '@jwp/ott-ui-react/src/styles/theme'; +@use '@jwp/ott-ui-react/src/styles/accessibility'; @use '@jwp/ott-ui-react/src/styles/mixins/responsive'; $small-button-height: 28px; @@ -31,15 +32,7 @@ $large-button-height: 40px; &:hover, &:focus { z-index: 1; - transform: scale(1.1); - } - - &:focus:not(:focus-visible):not(:hover) { - transform: scale(1); - } - - &:focus-visible { - transform: scale(1.1); + transform: scale(1.05); } } } @@ -65,6 +58,10 @@ $large-button-height: 40px; &.primary { color: var(--highlight-contrast-color, theme.$btn-primary-color); background-color: var(--highlight-color, theme.$btn-primary-bg); + + &:focus { + @include accessibility.accessibleOutlineContrast; + } } &.outlined { @@ -76,6 +73,7 @@ $large-button-height: 40px; color: var(--highlight-contrast-color, theme.$btn-primary-color); background-color: var(--highlight-color, theme.$btn-primary-bg); border-color: var(--highlight-color, theme.$btn-primary-bg); + outline: none; } } } diff --git a/packages/ui-react/src/components/Button/Button.tsx b/packages/ui-react/src/components/Button/Button.tsx index fc4bf0c72..842e92934 100644 --- a/packages/ui-react/src/components/Button/Button.tsx +++ b/packages/ui-react/src/components/Button/Button.tsx @@ -42,7 +42,7 @@ const Button: React.FC = ({ size = 'medium', disabled, busy, - type, + type = 'button', to, as = 'button', onClick, diff --git a/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap b/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap index 766a87a85..aa5e13b5a 100644 --- a/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap +++ b/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap @@ -4,6 +4,7 @@ exports[`
diff --git a/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap b/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap index e741084ab..e1f960683 100644 --- a/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap +++ b/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap @@ -14,6 +14,7 @@ exports[` > renders and matches snapshot 1`] = ` + + + + +`; diff --git a/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap b/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap index 6bf873a36..8fc29d1be 100644 --- a/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap +++ b/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap @@ -10,6 +10,7 @@ exports[` > renders Filter 1`] = `