From 69ee0ea1df036ccc2f49de44b743906c596b044a Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Thu, 7 Oct 2021 12:37:09 +0200 Subject: [PATCH 01/30] refactor: Mark library as side-effect-free, noissue --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e8ceb093..2ac07d39 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", + "sideEffects": false, "scripts": { "build-icons": "pixo src-icons/svg --out-dir src/icons --template src-icons/template.js", "compile": "tsc", From c777ec712d9e74d45a2888ea2bec59e1318b7c44 Mon Sep 17 00:00:00 2001 From: Andrew Dodson Date: Thu, 7 Oct 2021 13:53:35 +0100 Subject: [PATCH 02/30] chore(ci): fix broken dep cache --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9963705f..a74e92c5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,8 +8,8 @@ aliases: - &attach_workspace attach_workspace: at: *dir - - &cache-key dependency-cache-lts-{{ checksum "package.json" }} - - &cache-key-fallback dependency-cache- + - &cache-key dependency-cache-lts-a-{{ checksum "package.json" }} + - &cache-key-fallback dependency-cache-lts-a- ################################ # Executors From 6ff6309fb0b59843ff88495bdeb7146cd0a976c9 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Thu, 7 Oct 2021 13:03:36 +0000 Subject: [PATCH 03/30] chore(release): 13.4.9 [skip ci] ## [13.4.9](https://github.com/5app/base5-ui/compare/v13.4.8...v13.4.9) (2021-10-07) --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80728d49..6a68df67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## [13.4.9](https://github.com/5app/base5-ui/compare/v13.4.8...v13.4.9) (2021-10-07) + ## [13.4.8](https://github.com/5app/base5-ui/compare/v13.4.7...v13.4.8) (2021-09-14) diff --git a/package.json b/package.json index 45ff65d2..eff5f639 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "13.4.8", + "version": "13.4.9", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From b232306822560c88314d33a92e778c47f0f1d0cb Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Mon, 11 Oct 2021 16:33:06 +0200 Subject: [PATCH 04/30] feat: New useCallbackRef hook, noissue --- src/useCallbackRef/index.js | 27 +++++++++++++++++++++++++++ src/useEventListener/index.js | 11 ++++------- src/useFocusOnMount/index.js | 19 +++++++------------ 3 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 src/useCallbackRef/index.js diff --git a/src/useCallbackRef/index.js b/src/useCallbackRef/index.js new file mode 100644 index 00000000..8ab2a7d6 --- /dev/null +++ b/src/useCallbackRef/index.js @@ -0,0 +1,27 @@ +import {useRef, useEffect} from 'react'; + +/** + * A hook that returns a stable React ref for a function. + * The ref will always be kept up to date with the latest + * version of the callback. Can be used to work around issues + * with effects that have an incomplete dependency array. + * + * Usually it's best to avoid those, but sometimes this is + * not possible. + * + * @param {Function} callback - A function + * @returns a React ref object whose `current` property is set to + * the latest version of the callback function + */ + +function useCallbackRef(callback) { + const callbackRef = useRef(callback); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + return callbackRef; +} + +export default useCallbackRef; diff --git a/src/useEventListener/index.js b/src/useEventListener/index.js index d1e02ec6..b358661a 100644 --- a/src/useEventListener/index.js +++ b/src/useEventListener/index.js @@ -1,4 +1,5 @@ -import {useEffect, useRef} from 'react'; +import {useEffect} from 'react'; +import useCallbackRef from '../useCallbackRef'; /** * Add a (global) event listener to your component. @@ -12,14 +13,10 @@ import {useEffect, useRef} from 'react'; */ function useEventListener(eventName, callback, options = {}) { - const callbackRef = useRef(callback); + const callbackRef = useCallbackRef(callback); const {isEnabled = true, capture = false, targetElement} = options; - useEffect(() => { - callbackRef.current = callback; - }, [callback]); - useEffect(() => { const element = targetElement || document; const currentCallback = event => callbackRef.current(event); @@ -33,7 +30,7 @@ function useEventListener(eventName, callback, options = {}) { return function cleanUp() { element.removeEventListener(eventName, currentCallback, capture); }; - }, [isEnabled, eventName, targetElement, capture]); + }, [callbackRef, isEnabled, eventName, targetElement, capture]); } export default useEventListener; diff --git a/src/useFocusOnMount/index.js b/src/useFocusOnMount/index.js index 472c3956..55117a4a 100644 --- a/src/useFocusOnMount/index.js +++ b/src/useFocusOnMount/index.js @@ -1,4 +1,5 @@ import {useRef, useEffect} from 'react'; +import useCallbackRef from '../useCallbackRef'; function useFocusOnMount({ onFocus, @@ -9,16 +10,12 @@ function useFocusOnMount({ const innerRef = useRef(); const previouslyFocusedElementRef = useRef(); - const onFocusRef = useRef(onFocus); - const onRestoreFocusRef = useRef(onRestoreFocus); - - useEffect(() => { - onFocusRef.current = onFocus; - onRestoreFocusRef.current = onRestoreFocus; - }, [onFocus, onRestoreFocus]); + const onFocusRef = useCallbackRef(onFocus); + const onRestoreFocusRef = useCallbackRef(onRestoreFocus); useEffect(() => { const ref = userRef || innerRef; + const restoreFocus = onRestoreFocusRef.current; if (!disabled) { previouslyFocusedElementRef.current = document.activeElement; @@ -31,14 +28,12 @@ function useFocusOnMount({ return () => { if (!disabled) { previouslyFocusedElementRef.current?.focus(); - if (onRestoreFocusRef.current) { - onRestoreFocusRef.current( - previouslyFocusedElementRef.current - ); + if (restoreFocus) { + restoreFocus(previouslyFocusedElementRef.current); } } }; - }, [disabled, userRef]); + }, [disabled, onFocusRef, onRestoreFocusRef, userRef]); return innerRef; } From c28c599ef5148573def46e3fd520ea20f9828d74 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Mon, 11 Oct 2021 14:37:00 +0000 Subject: [PATCH 05/30] chore(release): 13.5.0 [skip ci] # [13.5.0](https://github.com/5app/base5-ui/compare/v13.4.9...v13.5.0) (2021-10-11) ### Features * New useCallbackRef hook, noissue ([b232306](https://github.com/5app/base5-ui/commit/b232306822560c88314d33a92e778c47f0f1d0cb)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a68df67..4584463c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [13.5.0](https://github.com/5app/base5-ui/compare/v13.4.9...v13.5.0) (2021-10-11) + + +### Features + +* New useCallbackRef hook, noissue ([b232306](https://github.com/5app/base5-ui/commit/b232306822560c88314d33a92e778c47f0f1d0cb)) + ## [13.4.9](https://github.com/5app/base5-ui/compare/v13.4.8...v13.4.9) (2021-10-07) ## [13.4.8](https://github.com/5app/base5-ui/compare/v13.4.7...v13.4.8) (2021-09-14) diff --git a/package.json b/package.json index eff5f639..2685c0b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "13.4.9", + "version": "13.5.0", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 7e20065a4cf959c2d40bf28565bd4d7764e882d1 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Mon, 11 Oct 2021 16:37:24 +0200 Subject: [PATCH 06/30] fix(useCallbackRef): Add missing export, noissue --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index afa6b406..99e0aec5 100644 --- a/src/index.js +++ b/src/index.js @@ -38,6 +38,7 @@ export { BackLinkContext, } from './useBackLink'; export {default as useBreakpoints} from './useBreakpoints'; +export {default as useCallbackRef} from './useCallbackRef'; export {default as useChartist} from './useChartist'; export {default as useEventListener} from './useEventListener'; export {default as useFocusOnMount} from './useFocusOnMount'; From e689a38d18bdbfbd1fe1613bbc5903308232cab7 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Mon, 11 Oct 2021 14:40:28 +0000 Subject: [PATCH 07/30] chore(release): 13.5.1 [skip ci] ## [13.5.1](https://github.com/5app/base5-ui/compare/v13.5.0...v13.5.1) (2021-10-11) ### Bug Fixes * **useCallbackRef:** Add missing export, noissue ([7e20065](https://github.com/5app/base5-ui/commit/7e20065a4cf959c2d40bf28565bd4d7764e882d1)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4584463c..8e8927dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [13.5.1](https://github.com/5app/base5-ui/compare/v13.5.0...v13.5.1) (2021-10-11) + + +### Bug Fixes + +* **useCallbackRef:** Add missing export, noissue ([7e20065](https://github.com/5app/base5-ui/commit/7e20065a4cf959c2d40bf28565bd4d7764e882d1)) + # [13.5.0](https://github.com/5app/base5-ui/compare/v13.4.9...v13.5.0) (2021-10-11) diff --git a/package.json b/package.json index 2685c0b4..cbb427ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "13.5.0", + "version": "13.5.1", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From a9a54461109098d310f1d9795195e75425556d50 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Tue, 2 Nov 2021 13:05:25 +0100 Subject: [PATCH 08/30] fix: Don't hide live regions outside of modals, noissue --- src/Modal/accessiblyHideModalBackground.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Modal/accessiblyHideModalBackground.js b/src/Modal/accessiblyHideModalBackground.js index 94b19c05..c4bbbd0d 100644 --- a/src/Modal/accessiblyHideModalBackground.js +++ b/src/Modal/accessiblyHideModalBackground.js @@ -4,11 +4,12 @@ import PropTypes from 'prop-types'; const PREVENT_ARIA_HIDDEN_ATTRIBUTE = 'data-prevent-aria-hidden'; -// hide everything except modalElement and elements with -// the PREVENT_ARIA_HIDDEN_ATTRIBUTE +// hide everything except modalElement, live regions, and +// elements with the PREVENT_ARIA_HIDDEN_ATTRIBUTE function hideForModal(modalElement) { return hideOthers([ modalElement, + ...document.querySelectorAll(`[aria-live]`), ...document.querySelectorAll(`[${PREVENT_ARIA_HIDDEN_ATTRIBUTE}]`), ]); } From 7a6e4cf466152a5f07e66319a07b42d8ac1a1264 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Tue, 2 Nov 2021 12:08:29 +0000 Subject: [PATCH 09/30] chore(release): 13.5.2 [skip ci] ## [13.5.2](https://github.com/5app/base5-ui/compare/v13.5.1...v13.5.2) (2021-11-02) ### Bug Fixes * Don't hide live regions outside of modals, noissue ([a9a5446](https://github.com/5app/base5-ui/commit/a9a54461109098d310f1d9795195e75425556d50)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e8927dc..291e6236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [13.5.2](https://github.com/5app/base5-ui/compare/v13.5.1...v13.5.2) (2021-11-02) + + +### Bug Fixes + +* Don't hide live regions outside of modals, noissue ([a9a5446](https://github.com/5app/base5-ui/commit/a9a54461109098d310f1d9795195e75425556d50)) + ## [13.5.1](https://github.com/5app/base5-ui/compare/v13.5.0...v13.5.1) (2021-10-11) diff --git a/package.json b/package.json index cbb427ec..758140b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "13.5.1", + "version": "13.5.2", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 3dbe001d0647aac559dc4f9fcb62066a708de46b Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Tue, 2 Nov 2021 14:20:09 +0100 Subject: [PATCH 10/30] fix(Modal): Fix live regions outside of modals --- src/Modal/Modal.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Modal/Modal.js b/src/Modal/Modal.js index 22b9a998..7186f384 100644 --- a/src/Modal/Modal.js +++ b/src/Modal/Modal.js @@ -99,7 +99,13 @@ function Modal({ id={name} {...otherProps} role="dialog" - aria-modal="true" + // We don't need the aria-modal property here, + // because we're "polyfilling" its effect (hiding + // background content) using the 'aria-hidden' library, + // which also gives us more control over it and allows + // us to exclude certain elements from being hidden. + // This solves issue https://github.com/5app/hub/issues/10949 + // aria-modal="true" > Date: Tue, 2 Nov 2021 13:23:17 +0000 Subject: [PATCH 11/30] chore(release): 13.5.3 [skip ci] ## [13.5.3](https://github.com/5app/base5-ui/compare/v13.5.2...v13.5.3) (2021-11-02) ### Bug Fixes * **Modal:** Fix live regions outside of modals ([3dbe001](https://github.com/5app/base5-ui/commit/3dbe001d0647aac559dc4f9fcb62066a708de46b)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 291e6236..b9453ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [13.5.3](https://github.com/5app/base5-ui/compare/v13.5.2...v13.5.3) (2021-11-02) + + +### Bug Fixes + +* **Modal:** Fix live regions outside of modals ([3dbe001](https://github.com/5app/base5-ui/commit/3dbe001d0647aac559dc4f9fcb62066a708de46b)) + ## [13.5.2](https://github.com/5app/base5-ui/compare/v13.5.1...v13.5.2) (2021-11-02) diff --git a/package.json b/package.json index 758140b3..7811695e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "13.5.2", + "version": "13.5.3", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 078807cf764c0d5df5aacc028eb29817b156323f Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Thu, 4 Nov 2021 17:46:29 +0100 Subject: [PATCH 12/30] fix: Small TS fix --- src/theme/types.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/theme/types.d.ts b/src/theme/types.d.ts index e6a8dc7c..3f9dcb4d 100644 --- a/src/theme/types.d.ts +++ b/src/theme/types.d.ts @@ -42,10 +42,10 @@ export interface ThemeGlobals { search?: number; }; readonly borderStyles: { - [name: string]: string | ((LocalThemeSection) => string); + [name: string]: string | ((themeSection: LocalThemeSection) => string); }; readonly shadowStyles: { - [name: string]: string | ((LocalThemeSection) => string); + [name: string]: string | ((themeSection: LocalThemeSection) => string); }; readonly colorBlocks: { [name: string]: ThemeColor; From ca9c45925e41b9119cb6351296cbcdb8f96c4da8 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Thu, 4 Nov 2021 17:48:20 +0100 Subject: [PATCH 13/30] feat(useBackLink): Big useBackLink refactor, #164 BREAKING CHANGE: This changes the public API of useBackLink and BackLinkProvider. --- src/useBackLink/BackLinkProvider.js | 169 -------------------------- src/useBackLink/BackLinkProvider.tsx | 149 +++++++++++++++++++++++ src/useBackLink/README.stories.mdx | 86 +++++++------ src/useBackLink/findInHistoryStack.js | 33 ----- src/useBackLink/getBackLinks.js | 43 ------- src/useBackLink/index.js | 11 -- src/useBackLink/index.ts | 52 ++++++++ src/utils/index.ts | 4 + src/utils/slicePathSegments.ts | 22 ++++ 9 files changed, 268 insertions(+), 301 deletions(-) delete mode 100644 src/useBackLink/BackLinkProvider.js create mode 100644 src/useBackLink/BackLinkProvider.tsx delete mode 100644 src/useBackLink/findInHistoryStack.js delete mode 100644 src/useBackLink/getBackLinks.js delete mode 100644 src/useBackLink/index.js create mode 100644 src/useBackLink/index.ts create mode 100644 src/utils/slicePathSegments.ts diff --git a/src/useBackLink/BackLinkProvider.js b/src/useBackLink/BackLinkProvider.js deleted file mode 100644 index 73c8b8ca..00000000 --- a/src/useBackLink/BackLinkProvider.js +++ /dev/null @@ -1,169 +0,0 @@ -import React, {createContext, useEffect, useState, useRef} from 'react'; -import PropTypes from 'prop-types'; - -import findInHistoryStack from './findInHistoryStack'; -import getBackLinks from './getBackLinks'; - -const BackLinkContext = createContext(false); - -function removePreviousEntry(history, indexes) { - if (!indexes) { - return history; - } - const newHistory = [...history]; - const [x, y] = indexes; - if (newHistory[x]) { - newHistory[x].splice(y, 1); - if (newHistory[x].length === 0) { - newHistory[x] = undefined; - } - } - return newHistory; -} - -function BackLinkProvider({ - children, - location, - track, - getLocationLevel, - getLocationId, - wasRedirected, -}) { - const [historyStack, setHistoryStack] = useState([]); - const indexesOfPreviousEntry = useRef(null); - const haveTrackedDependenciesChanged = useRef(false); - - // We track dependencies in this separate effect because - // the main effect needs to run every time the location changes. - // This allows us to only create _new_ history entries when - // a tracked dependency changes, but to keep the current - // location entry up to date with all changes, tracked or not. - useEffect(() => { - haveTrackedDependenciesChanged.current = true; - }, track); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - const wrappedLocation = { - location, - id: getLocationId(track), - }; - const historyStackIndexes = findInHistoryStack( - wrappedLocation, - historyStack - ); - - const [locationLevel, entryIndex] = historyStackIndexes || [ - getLocationLevel(location), - 0, - ]; - - setHistoryStack(prevHistory => { - let newHistory = [...prevHistory]; - - // If the tracked dependencies haven't changed, we only - // update the current location with its latest state, so - // that going back always restores the last viewed URL state - if (!haveTrackedDependenciesChanged.current) { - newHistory[locationLevel][entryIndex] = wrappedLocation; - - return newHistory; - } - - if (!historyStackIndexes) { - // If location wasn't found in history - if (newHistory[locationLevel]) { - // If entries exist at the location's level, append it - newHistory[locationLevel] = [ - ...newHistory[locationLevel], - wrappedLocation, - ]; - } else { - // If no entries exist, create a new level - newHistory[locationLevel] = [wrappedLocation]; - } - } else { - // The location already exists somewhere in the history stack, - // so we discard any entries after it - newHistory[locationLevel] = newHistory[locationLevel].slice( - 0, - entryIndex + 1 - ); - } - - // Discard any higher levels - newHistory = newHistory.slice(0, locationLevel + 1); - - // If we were redirected to this location, discard - // the previous entry so we can skip the redirect - if (wasRedirected) { - newHistory = removePreviousEntry( - newHistory, - indexesOfPreviousEntry.current - ); - // Set to null so we don't try to remove it again - indexesOfPreviousEntry.current = null; - } else { - indexesOfPreviousEntry.current = [ - locationLevel, - newHistory[locationLevel].length - 1, - ]; - } - - return newHistory; - }); - - return () => { - haveTrackedDependenciesChanged.current = false; - }; - }, [location]); // eslint-disable-line react-hooks/exhaustive-deps - - const backLinks = getBackLinks(historyStack); - - return ( - - {children} - - ); -} - -BackLinkProvider.defaultProps = { - getLocationLevel: () => 0, - getLocationId: track => track.join(','), -}; - -BackLinkProvider.propTypes = { - /** - * A string or object identifying the current location on your site. - * This is what will be returned from the `useBackLink` hook, - * so make sure you can construct a proper link URL from it. - */ - location: PropTypes.oneOfType([PropTypes.object, PropTypes.string]) - .isRequired, - /** - * A "dependency array" of values that tell the component whether or - * not a new entry should be added for the current location. - * A new entry will be added only if one or more of the array's items have changed. - * All requirements & limitations of `React.useEffect` dependency arrays apply here, too. - */ - track: PropTypes.array.isRequired, - /** - * A function that's called with the current location and should - * return a "hierarchy level" for the location - */ - getLocationLevel: PropTypes.func, - /** - * A function that's called with the `track` array and should - * return a unique ID from those values - */ - getLocationId: PropTypes.func, - /** - * Indicate that the current location was the result of a redirect. - * This will cause the history entry that initiated the redirect - * (i.e. the previous one) to be discarded. - */ - wasRedirected: PropTypes.bool, -}; - -export {BackLinkContext}; - -export default BackLinkProvider; diff --git a/src/useBackLink/BackLinkProvider.tsx b/src/useBackLink/BackLinkProvider.tsx new file mode 100644 index 00000000..75aad606 --- /dev/null +++ b/src/useBackLink/BackLinkProvider.tsx @@ -0,0 +1,149 @@ +import React, {createContext, useEffect, useState, useRef} from 'react'; + +interface LocationObject { + readonly [name: string]: unknown; +} + +export type Location = string | LocationObject; + +interface HistoryEntry { + readonly id: string; + readonly location: Location; +} + +type TrackedVariables = unknown[]; + +interface BackLinkProviderProps { + children: React.ReactNode; + /** + * A string or object identifying the current location on your site. + * This is what will be returned from the `useBackLink` hook, + * so make sure you can construct a proper link URL from it. + */ + location: Location; + /** + * A "dependency array" of values that tell the component whether or + * not a new entry should be added for the current location. + * A new entry will be added only if one or more of the array's items have changed. + * All requirements & limitations of `React.useEffect` dependency arrays apply here, too. + */ + track: TrackedVariables; + /** + * A function that's called with the `track` array and should + * return a unique ID from those values + */ + getLocationId?: (track: TrackedVariables) => string; + /** + * A function that's called with a location and should + * return the pathname string + */ + getLocationPathname?: (location: Location) => string; + /** + * Indicate that the current location was the result of a redirect. + * This will cause the history entry that initiated the redirect + * (i.e. the previous one) to be discarded. + */ + wasRedirected?: boolean; +} + +interface HistoryContext { + historyStack: HistoryEntry[]; + getLocationPathname: (location: Location) => string; +} + +export const BackLinkContext = createContext({ + historyStack: [], + getLocationPathname: null, +}); + +function BackLinkProvider({ + children, + location, + track, + getLocationId, + getLocationPathname, + wasRedirected, +}: BackLinkProviderProps): React.ReactNode { + const [historyStack, setHistoryStack] = useState([]); + + const previousLocationIndex = useRef(null); + const haveTrackedDependenciesChanged = useRef(false); + + // We track dependencies in this separate effect because + // the main effect needs to run every time the location changes. + // This allows us to only create _new_ history entries when + // a tracked dependency changes, but to also keep the current + // location entry up to date with all changes, tracked or not. + useEffect(() => { + haveTrackedDependenciesChanged.current = true; + }, track); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + const wrappedLocation = { + location, + id: getLocationId(track), + }; + let locationIndex = historyStack.findIndex( + entry => entry.id === wrappedLocation.id + ); + + setHistoryStack(prevHistory => { + let newHistory = [...prevHistory]; + + if (locationIndex === -1) { + // If this is a new location, add it as a new entry + newHistory = [...newHistory, wrappedLocation]; + locationIndex = newHistory.length - 1; + } else { + // If the tracked dependencies haven't changed, we only + // update the current location with its latest state, so + // that going back always restores the last viewed URL state + if (!haveTrackedDependenciesChanged.current) { + newHistory[locationIndex] = wrappedLocation; + + return newHistory; + } + + // The location already exists in the history stack, + // so we discard any entries after it + newHistory = newHistory.slice(0, locationIndex + 1); + } + + const previousIndex = previousLocationIndex.current; + + // If we were redirected to this location, discard + // the previous entry so we can skip the redirect + if ( + wasRedirected && + previousIndex !== null && + newHistory[previousIndex] + ) { + newHistory = newHistory.slice(previousIndex, previousIndex + 1); + + // Set to null so we don't try to remove it again + previousLocationIndex.current = null; + } else { + previousLocationIndex.current = locationIndex; + } + + return newHistory; + }); + + return () => { + haveTrackedDependenciesChanged.current = false; + }; + }, [location]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + {children} + + ); +} + +BackLinkProvider.defaultProps = { + getLocationId: track => track.join(','), + getLocationPathname: location => location?.pathname || location, +}; + +export default BackLinkProvider; diff --git a/src/useBackLink/README.stories.mdx b/src/useBackLink/README.stories.mdx index 5dc1db80..524883cf 100644 --- a/src/useBackLink/README.stories.mdx +++ b/src/useBackLink/README.stories.mdx @@ -10,7 +10,7 @@ import useBackLink, {BackLinkProvider} from './'; import useBackLink, {BackLinkProvider} from 'base5-ui/useBackLink'; ``` -`useBackLink` is a hook that returns a link to the previous page that the user has visited (if available). It can track the user's history across multiple hierarchy levels to avoid circular navigation issues, and allows navigating "up" a page level instead of only "back" to the last page viewed. +`useBackLink` is a hook that returns a link to the previous page that the user has visited (if available). It also makes it easy to avoid circular navigation issues. ## The problem @@ -18,28 +18,22 @@ Hard-coding back links in an app where pages may have different entry points can It might be tempting to just mirror the browser's back button behaviour using `history.back` to create back buttons on your site. However, this comes with several downsides: -1. It doesn't work when there's no browser history, for example after opening a new browser window. If your user opened a link in an email to arrive on a page with a button wired up to `history.back`, that button would simply do nothing. It would be much better if it took them to an overview page or another fallback location instead. +1. It doesn't work when there's no browser history, for example after opening a new browser window. If your user opened a link in an email to arrive on a page with a button wired up to `history.back`, that button would simply do nothing. Boo! -1. Conflicts with manual navigation. A user may navigate back on your site by clicking a regular link, which will be registered by the browser as "forward navigation". This means that a new history entry will be created even though the user has navigated backwards relative to your page's hierarchy or structure. +1. It can conflict with manual navigation. A user may navigate "back" on your site by clicking a regular link, which will be registered by the browser as "forward navigation". This means that a new history entry will be created even though the user has navigated backwards relative to your page's hierarchy or structure. -1. Circular navigation: While users expect their _browser_ to take them back exactly where they were when they click the browser's back button, they don't necessarily expect back links on your site to work the same way. For example, they might expect them to match the site's hierarchy instead. Imagine navigating from the homepage of a web shop to a category page. On the category page, you click on a product, then return to the category page. If that category page had a "Back" button, where would you expect it to take you? Back to the product you just viewed, or back to the homepage? I'd suspect that most users would expect to be taken back to the homepage (i.e. higher up in the site hierarchy), yet `history.back` would simply take them back to the product. +1. Circular navigation: While users expect their _browser_ to take them back exactly where they were when they click the browser's back button, they don't necessarily expect back links on your site to work the same way. A user is more likely to expect a back link on a web site to match the site's hierarchy instead. Imagine navigating from the homepage of a web shop to a category page. On the category page, you click on a product, then return to the category page. If that category page had a "Back" button, where would you expect it to take you? Back to the product you just viewed, or back to the homepage? I'd argue that most users would expect to be taken back to the homepage (i.e. higher up in the site hierarchy), yet `history.back` would simply take them back to the product. - Of course the back link on the category page could be hard-coded to always point to the homepage, but what if categories could be navigated to from various other pages in the shop? + Of course the back link on the category page could be hard-coded to always point to the homepage, but this won't hold up if categories can be navigated to from various other pages in the shop. -## The solution - -The base5-ui `useBackLink` hook and `BackLinkProvider` component were built to handle this sort of complexity in a maintainable way. They allow you to assign "levels" to your routes to correctly keep track of the user's browsing history through the hierarchy of your page. - -Sticking with our previous example, the shop's homepage and all its siblings would be assigned a level of `0`, the category page a level of `1`, and the product pages a level of `2`. - -The following section will describe what the set up for this would look like: +Back links are a surprisingly hard problem to solve on web sites and apps! The base5-ui `useBackLink` hook and `BackLinkProvider` component were built to handle this sort of complexity in a maintainable way. ## Walkthrough and examples -To enable the most basic usage of the hook, you need to wrap your app with the `BackLinkProvider` component like this: +To enable the most basic usage of the hook, you need to wrap your app with the `BackLinkProvider` component as shown below. Make sure that the provider isn't unmounted as users navigate your app, so it should be placed below your router provider (so you can access the current location), but above your routes. ```js -import {useLocation, Link} from 'react-router'; +import {useLocation, Link} from 'react-router-dom'; import {BackLinkProvider} from 'base5-ui/useBackLink'; function YourApp({children}) { @@ -51,9 +45,9 @@ function YourApp({children}) { ); } -function BackLink({fallbackLink, children}) { +function BackLink({children}) { const backLink = useBackLink(); - return {children}; + return {children}; } ``` @@ -75,45 +69,48 @@ return ( ); ``` -Now that `BackLinkProvider` knows when to update its history, it'll keep track of the user's navigation in a linear fashion. It doesn't work the same as the browser's history, though: To avoid the circular navigation issues mentioned earlier, our history doesn't allow duplicate entries. So as soon as the user navigates to a page that already exists in the history, the existing page will be made the new user location, and any later history entries will be discarded. +> Note: If you simply want to track all and any location changes without specifying individual parameters, you can pass the whole location object to the `track` array, too. In that case, also add the `getLocationId` prop that's used to turn the `track` array into a unique string. For react-router's location object, you could set it to `([location]) => location.key`. -### Hierarchy-aware history +Now that `BackLinkProvider` knows when to update its history, it'll keep track of the user's navigation in a linear fashion. It doesn't work exactly the same as the browser's history, though: To avoid the circular navigation issues mentioned earlier, our history doesn't allow duplicate entries. So as soon as the user navigates to a page that already exists in the history, the existing page will be made the new user location, and any later history entries will be discarded. -If we want to take our site's hierarchy into account, we also need to pass the Provider compoent a function that, given the current location, will return that location's hierarchy level. This function should be passed to the `getLocationLevel` prop as shown below. (Note the use of the `matchPath` helper from react-router, which helps us to find out which route we're on.) +### Back link fallbacks -```js -import {matchPath} from 'react-router'; +When there's no page in the history to navigate back to (for example because it's the first page the user has visited), you may still want to render a functioning back link. Pass a fallback option to the hook, and it will return this link whenever there's no history to draw from: -return ( - { - if (matchPath(pathname, {path: '/product/:id'})) { - return 2; - } else if (matchPath(pathname, {path: '/category/:id'})) { - return 1; - } else { - return 0; - } - }} - > - {children} - -); +```js +const backLink = useBackLink({fallback: '/home'}); ``` -Now, the `useBackLink` hook will return a link to the previously visited page of the current hierarchy level – if there is one. If there's no previous page on the current level, it'll point to the last page of the _previous_ hierarchy level. +### Hierarchy-aware history -You can also ask the hook to _always_ return a link from a higher hierarchy level. This is useful to avoid the circular navigation issue described earlier, for example on overview pages that have local navigation (think tabs or a side bar with links). Make sure that all sibling pages report the same hierarchy, and a back link wired up as shown below will always point to the last page viewed on the nearest hierarchy level, ignoring any local navigation that has taken place. +Sometimes, you may have a back link in the header of a page that has subpages, each with their own nested URL. And you may want that back link to always point to a previously visited _parent_ page, so that users don't have to click the back link once for each subpage they visited. -```js -function BackLink({fallback, children}) { - const backLink = useBackLink({up: true}); - return {children}; +The `basePath` option let's you implement this. In the example below, `backLink` will never resolve to any URLs that start with `'/products'`. + +```jsx +function ProductsPage() { + const backLink = useBackLink({fallback: '/home', basePath: '/products'}); + + return ( + <> + Back + Product A + Product B + Product C + + ); } ``` +There's also an option for more complex cases that can't be solved by a static base path. `shouldSkipLocation` is a function that's called for any "back link candidate", and if you return `true` from it, that candidate will be skipped, and an earlier history entry (or the fallback) will be returned instead. + +```jsx +const backLink = useBackLink({ + fallback: '/home', + shouldSkipLocation: location => location.startsWith('/products'), +}); +``` + ## Handling automatic redirects If a user is automatically redirected from one URL to another, two entries will be created in the back link history. This is a problem: If the user navigates back to the first entry, the redirect will be re-triggered, and the user ends up back on the page they just tried to navigate away from! @@ -133,7 +130,6 @@ Note that the location passed to `BackLinkProvider` **must not** include the 'wa } }} track={[pathname, modal, page]} - getLocationLevel={getLocationLevel} > {children} diff --git a/src/useBackLink/findInHistoryStack.js b/src/useBackLink/findInHistoryStack.js deleted file mode 100644 index 3b73f037..00000000 --- a/src/useBackLink/findInHistoryStack.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Finds a location in a nested "history stack" array, - * and returns its indexes as a tuple ([x, y]) - * - * @param {object} location - location object - * @param {string} location.id - unique identifier of the stored location - * @param {Array[]} historyStack - nested array, containing location objects - * - * @returns {Array|null} - A tuple [x, y] of the location's index in the array - * if found, or null if not found. - * If found, the location can be accessed at historyStack[x][y]. - */ - -function findInHistoryStack(location, historyStack) { - for (let level = historyStack.length; level >= 0; level--) { - if (historyStack[level]) { - for ( - let locIndex = historyStack[level].length; - locIndex >= 0; - locIndex-- - ) { - if (historyStack[level][locIndex]) { - if (historyStack[level][locIndex].id === location.id) { - return [level, locIndex]; - } - } - } - } - } - return null; -} - -export default findInHistoryStack; diff --git a/src/useBackLink/getBackLinks.js b/src/useBackLink/getBackLinks.js deleted file mode 100644 index 164f63f0..00000000 --- a/src/useBackLink/getBackLinks.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @typedef {object} backlinks - * @property {object|string} backLink - * @property {object|string} backUpLink - */ - -/** - * Returns two types of back links from the current history stack. - * 1. backLink: The previous user location at the current - * hierarchy level, or the previous location at the nearest hierarchy level - * 2. backUpLink: The previus user location at the nearest hierarchy level - * - * @param {Array[]} historyStack - nested array, containing location objects - * - * @returns {backlinks} - */ - -function getBackLinks(historyStack) { - const hasHistory = historyStack.length > 1 || historyStack[0]?.length > 1; - // The previous page on the current history level - let backLink; - // The last page on the previous history level - let backUpLink; - - if (hasHistory) { - // We're looking for the "back up" link first. - // There can be gaps between levels in the history stack, - // so we need to filter those out first - const gaplessHistory = historyStack.filter(Boolean); - const secondToLastLevel = gaplessHistory[gaplessHistory.length - 2]; - if (secondToLastLevel) { - backUpLink = - secondToLastLevel[secondToLastLevel.length - 1]?.location; - } - - const lastLevel = historyStack[historyStack.length - 1]; - backLink = lastLevel[lastLevel.length - 2]?.location || backUpLink; - } - - return {backLink, backUpLink}; -} - -export default getBackLinks; diff --git a/src/useBackLink/index.js b/src/useBackLink/index.js deleted file mode 100644 index 5cb67609..00000000 --- a/src/useBackLink/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import {useContext} from 'react'; -import BackLinkProvider, {BackLinkContext} from './BackLinkProvider'; - -function useBackLink({up} = {}) { - const {backLink, backUpLink} = useContext(BackLinkContext); - return up ? backUpLink : backLink; -} - -export {BackLinkProvider, BackLinkContext}; - -export default useBackLink; diff --git a/src/useBackLink/index.ts b/src/useBackLink/index.ts new file mode 100644 index 00000000..7890c5de --- /dev/null +++ b/src/useBackLink/index.ts @@ -0,0 +1,52 @@ +import {useContext} from 'react'; +import BackLinkProvider, {BackLinkContext, Location} from './BackLinkProvider'; + +import {stripSlashFromEnd} from '../utils'; + +interface BackLinkOptions { + fallback?: Location; + basePath?: string; + shouldSkipLocation?: (location: Location) => boolean; +} + +function useBackLink(options: BackLinkOptions = {}): Location | null { + const {fallback, basePath, shouldSkipLocation} = options; + + const {historyStack, getLocationPathname} = useContext(BackLinkContext); + + if (historyStack.length <= 1) { + return fallback || null; + } + + function getShouldSkipLocation(location) { + const ignoreSubpath = basePath?.startsWith( + stripSlashFromEnd(getLocationPathname(location)) + ); + + const skipLocation = shouldSkipLocation?.(location); + + return ignoreSubpath || skipLocation; + } + + function getBackLink() { + // Traverse the history stack in reverse + // until a suitable backlink is found + for (let i = historyStack.length - 2; i >= 0; i--) { + const location = historyStack[i].location; + + if (!getShouldSkipLocation(location)) { + return location; + } + } + } + + const backLink = getBackLink(); + + return backLink || fallback; +} + +export {BackLinkProvider, BackLinkContext}; + +export {slicePathSegments} from '../utils'; + +export default useBackLink; diff --git a/src/utils/index.ts b/src/utils/index.ts index bce15cbf..27d8622d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,6 +5,10 @@ export {default as getLength} from './getLength'; export {default as getScrollParent} from './getScrollParent'; export {default as getSpacing} from './getSpacing'; export {default as scrollIntoViewIfNeeded} from './scrollIntoViewIfNeeded'; +export { + default as slicePathSegments, + stripSlashFromEnd, +} from './slicePathSegments'; export * from './spacing'; export {default as truncateText} from './truncateText'; export * from './units'; diff --git a/src/utils/slicePathSegments.ts b/src/utils/slicePathSegments.ts new file mode 100644 index 00000000..7c64deeb --- /dev/null +++ b/src/utils/slicePathSegments.ts @@ -0,0 +1,22 @@ +function slicePathSegments( + pathname: string, + start: number, + end?: number +): string { + const startsWithSlash = pathname[0] === '/'; + + const slicedPathname = pathname + .split('/') + .filter(Boolean) + .slice(start, end) + .join('/'); + + return startsWithSlash ? `/${slicedPathname}` : slicedPathname; +} + +export function stripSlashFromEnd(pathname: string): string { + const endsWithSlash = pathname[pathname.length - 1] === '/'; + return endsWithSlash ? pathname.slice(0, -1) : pathname; +} + +export default slicePathSegments; From 936c1467b26b09f9d6b28d828aa2514b312ea1c5 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Thu, 4 Nov 2021 16:51:51 +0000 Subject: [PATCH 14/30] chore(release): 14.0.0 [skip ci] # [14.0.0](https://github.com/5app/base5-ui/compare/v13.5.3...v14.0.0) (2021-11-04) ### Bug Fixes * Small TS fix ([078807c](https://github.com/5app/base5-ui/commit/078807cf764c0d5df5aacc028eb29817b156323f)) ### Features * **useBackLink:** Big useBackLink refactor, [#164](https://github.com/5app/base5-ui/issues/164) ([ca9c459](https://github.com/5app/base5-ui/commit/ca9c45925e41b9119cb6351296cbcdb8f96c4da8)) ### BREAKING CHANGES * **useBackLink:** This changes the public API of useBackLink and BackLinkProvider. --- CHANGELOG.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9453ac6..acd3ce92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# [14.0.0](https://github.com/5app/base5-ui/compare/v13.5.3...v14.0.0) (2021-11-04) + + +### Bug Fixes + +* Small TS fix ([078807c](https://github.com/5app/base5-ui/commit/078807cf764c0d5df5aacc028eb29817b156323f)) + + +### Features + +* **useBackLink:** Big useBackLink refactor, [#164](https://github.com/5app/base5-ui/issues/164) ([ca9c459](https://github.com/5app/base5-ui/commit/ca9c45925e41b9119cb6351296cbcdb8f96c4da8)) + + +### BREAKING CHANGES + +* **useBackLink:** This changes the public API of useBackLink and +BackLinkProvider. + ## [13.5.3](https://github.com/5app/base5-ui/compare/v13.5.2...v13.5.3) (2021-11-02) diff --git a/package.json b/package.json index 7811695e..3e8f0df1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "13.5.3", + "version": "14.0.0", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 41648a84d2552c0a6ed63c0dab2c27aebbfd805a Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Fri, 5 Nov 2021 12:58:57 +0100 Subject: [PATCH 15/30] fix(useBackLink): Fix inverted subpath logic, #164 --- src/useBackLink/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/useBackLink/index.ts b/src/useBackLink/index.ts index 7890c5de..5f9159ea 100644 --- a/src/useBackLink/index.ts +++ b/src/useBackLink/index.ts @@ -19,9 +19,11 @@ function useBackLink(options: BackLinkOptions = {}): Location | null { } function getShouldSkipLocation(location) { - const ignoreSubpath = basePath?.startsWith( - stripSlashFromEnd(getLocationPathname(location)) - ); + const formattedLocation = basePath + ? stripSlashFromEnd(getLocationPathname(location)) + : null; + + const ignoreSubpath = formattedLocation?.startsWith(basePath); const skipLocation = shouldSkipLocation?.(location); @@ -42,6 +44,8 @@ function useBackLink(options: BackLinkOptions = {}): Location | null { const backLink = getBackLink(); + console.log(basePath, historyStack); + return backLink || fallback; } From 5ab21b8902646ccabc36285d9eee66171f15742c Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Fri, 5 Nov 2021 15:44:56 +0100 Subject: [PATCH 16/30] fix(BackLinkProvider): Fix slice/splice mixup, #164 --- src/useBackLink/BackLinkProvider.tsx | 2 +- src/useBackLink/index.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/useBackLink/BackLinkProvider.tsx b/src/useBackLink/BackLinkProvider.tsx index 75aad606..96e4596b 100644 --- a/src/useBackLink/BackLinkProvider.tsx +++ b/src/useBackLink/BackLinkProvider.tsx @@ -118,7 +118,7 @@ function BackLinkProvider({ previousIndex !== null && newHistory[previousIndex] ) { - newHistory = newHistory.slice(previousIndex, previousIndex + 1); + newHistory.splice(previousIndex, 1); // Set to null so we don't try to remove it again previousLocationIndex.current = null; diff --git a/src/useBackLink/index.ts b/src/useBackLink/index.ts index 5f9159ea..e8fa3554 100644 --- a/src/useBackLink/index.ts +++ b/src/useBackLink/index.ts @@ -44,8 +44,6 @@ function useBackLink(options: BackLinkOptions = {}): Location | null { const backLink = getBackLink(); - console.log(basePath, historyStack); - return backLink || fallback; } From 72485cadea51bd7629efd31b3a7ddd473502b879 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Fri, 5 Nov 2021 15:57:13 +0100 Subject: [PATCH 17/30] fix(BackLinkProvider): Simplify redirect handling, #164 --- src/useBackLink/BackLinkProvider.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/useBackLink/BackLinkProvider.tsx b/src/useBackLink/BackLinkProvider.tsx index 96e4596b..f24d1aaa 100644 --- a/src/useBackLink/BackLinkProvider.tsx +++ b/src/useBackLink/BackLinkProvider.tsx @@ -66,7 +66,6 @@ function BackLinkProvider({ }: BackLinkProviderProps): React.ReactNode { const [historyStack, setHistoryStack] = useState([]); - const previousLocationIndex = useRef(null); const haveTrackedDependenciesChanged = useRef(false); // We track dependencies in this separate effect because @@ -83,11 +82,12 @@ function BackLinkProvider({ location, id: getLocationId(track), }; - let locationIndex = historyStack.findIndex( - entry => entry.id === wrappedLocation.id - ); setHistoryStack(prevHistory => { + let locationIndex = prevHistory.findIndex( + entry => entry.id === wrappedLocation.id + ); + let newHistory = [...prevHistory]; if (locationIndex === -1) { @@ -109,21 +109,16 @@ function BackLinkProvider({ newHistory = newHistory.slice(0, locationIndex + 1); } - const previousIndex = previousLocationIndex.current; + const previousIndex = newHistory.length - 2; // If we were redirected to this location, discard // the previous entry so we can skip the redirect if ( wasRedirected && - previousIndex !== null && + previousIndex <= 0 && newHistory[previousIndex] ) { newHistory.splice(previousIndex, 1); - - // Set to null so we don't try to remove it again - previousLocationIndex.current = null; - } else { - previousLocationIndex.current = locationIndex; } return newHistory; From 1341ba23dd74228585beee4593e500900a48db3f Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Fri, 5 Nov 2021 15:00:06 +0000 Subject: [PATCH 18/30] chore(release): 14.0.1 [skip ci] ## [14.0.1](https://github.com/5app/base5-ui/compare/v14.0.0...v14.0.1) (2021-11-05) ### Bug Fixes * **BackLinkProvider:** Fix slice/splice mixup, [#164](https://github.com/5app/base5-ui/issues/164) ([5ab21b8](https://github.com/5app/base5-ui/commit/5ab21b8902646ccabc36285d9eee66171f15742c)) * **BackLinkProvider:** Simplify redirect handling, [#164](https://github.com/5app/base5-ui/issues/164) ([72485ca](https://github.com/5app/base5-ui/commit/72485cadea51bd7629efd31b3a7ddd473502b879)) * **useBackLink:** Fix inverted subpath logic, [#164](https://github.com/5app/base5-ui/issues/164) ([41648a8](https://github.com/5app/base5-ui/commit/41648a84d2552c0a6ed63c0dab2c27aebbfd805a)) --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd3ce92..0969a90c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [14.0.1](https://github.com/5app/base5-ui/compare/v14.0.0...v14.0.1) (2021-11-05) + + +### Bug Fixes + +* **BackLinkProvider:** Fix slice/splice mixup, [#164](https://github.com/5app/base5-ui/issues/164) ([5ab21b8](https://github.com/5app/base5-ui/commit/5ab21b8902646ccabc36285d9eee66171f15742c)) +* **BackLinkProvider:** Simplify redirect handling, [#164](https://github.com/5app/base5-ui/issues/164) ([72485ca](https://github.com/5app/base5-ui/commit/72485cadea51bd7629efd31b3a7ddd473502b879)) +* **useBackLink:** Fix inverted subpath logic, [#164](https://github.com/5app/base5-ui/issues/164) ([41648a8](https://github.com/5app/base5-ui/commit/41648a84d2552c0a6ed63c0dab2c27aebbfd805a)) + # [14.0.0](https://github.com/5app/base5-ui/compare/v13.5.3...v14.0.0) (2021-11-04) diff --git a/package.json b/package.json index 3e8f0df1..fdb83b87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "14.0.0", + "version": "14.0.1", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 75247dc632530db2c07cbdce7aac20fa097c216e Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Tue, 9 Nov 2021 15:57:28 +0100 Subject: [PATCH 19/30] fix: Work around styles-components parent selector issue, noissue Adds double ampersands to work around styled-components issue #3265: https://github.com/styled-components/styled-components/issues/3265 --- src/Button/index.js | 10 ++++----- src/ButtonCore/index.js | 6 ++++-- src/MenuList/index.js | 21 ++++++++++++------- src/Pill/index.js | 8 +++---- src/Table/shared/RowSelectButton.js | 4 ++-- .../TableColumnHeader/ClickableHeader.js | 4 ++-- src/Table/shared/TableRowHeader.js | 10 ++++----- 7 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/Button/index.js b/src/Button/index.js index 796b1baf..a2387007 100644 --- a/src/Button/index.js +++ b/src/Button/index.js @@ -178,7 +178,7 @@ const FocusRing = styled.span.withConfig({ transition: opacity 250ms linear; will-change: opacity; - ${Wrapper}.focus-visible > & { + ${Wrapper}.focus-visible > && { top: -${_3px}; left: -${_3px}; bottom: -${_3px}; @@ -194,7 +194,7 @@ const FocusRing = styled.span.withConfig({ transition-duration: 50ms; } /* prettier-ignore */ - ${Wrapper}:not(.is-disabled):active > & { + ${Wrapper}:not(.is-disabled):active > && { opacity: 1; transition-duration: 50ms; } @@ -216,12 +216,12 @@ const HoverShade = styled.span` box-shadow: inset 0 0 0.25rem rgba(0, 0, 0, 0.5); } /* prettier-ignore */ - ${Wrapper}:not(.is-disabled):hover > & { + ${Wrapper}:not(.is-disabled):hover > && { opacity: 1; transition-duration: 50ms; } /* prettier-ignore */ - ${Wrapper}:not(.is-disabled):active > & { + ${Wrapper}:not(.is-disabled):active > && { opacity: 0; transition-duration: 250ms; } @@ -235,7 +235,7 @@ const Content = styled.span` `; /** - * The button label should just have some horinzontal spacing. + * The button label should just have some horizontal spacing. * But if `textOverflow="ellipsis" is set, the Button label * will have overflow set to "hidden", which can cause the * descenders of some characters (g, y, j) to be cut off. diff --git a/src/ButtonCore/index.js b/src/ButtonCore/index.js index 0889b1cf..a38eb65e 100644 --- a/src/ButtonCore/index.js +++ b/src/ButtonCore/index.js @@ -33,11 +33,13 @@ const Clickable = styled.button` cursor: pointer; } - &:focus:not(.focus-visible) { + &:focus:not(.focus-visible), + &:focus:not([data-focus-visible-added]) { outline: none; } - &.focus-visible { + &.focus-visible, + &[data-focus-visible-added] { outline: 3px solid currentColor; outline-offset: 3px; } diff --git a/src/MenuList/index.js b/src/MenuList/index.js index f4718c47..97fd5c33 100644 --- a/src/MenuList/index.js +++ b/src/MenuList/index.js @@ -31,10 +31,14 @@ const Item = styled.li` display: block; `; -const Link = styled(ButtonCore).withConfig({ - shouldForwardProp: prop => - ![...textLinkProps, 'isHighlighted'].includes(prop), -})` +const Link = styled(ButtonCore) + .attrs(({isHighlighted}) => ({ + className: isHighlighted ? 'is-highlighted' : '', + })) + .withConfig({ + shouldForwardProp: prop => + ![...textLinkProps, 'isHighlighted'].includes(prop), + })` position: relative; display: flex; align-items: center; @@ -57,7 +61,8 @@ const Link = styled(ButtonCore).withConfig({ outline: none; } - &.focus-visible:focus { + ${Wrapper}[data-focus-visible-added] &&.is-highlighted, + &[data-focus-visible-added]:focus { outline: none; &::after { @@ -106,9 +111,9 @@ const IconWrapper = styled.span` opacity: ${p => p.theme.textDimStrength}; - ${Link}:hover:not(.is-disabled) > &, - .is-focused:not(.is-disabled) > &, - .is-active:not(.is-disabled) > & { + ${Link}:hover:not(.is-disabled) > &&, + ${Link}[data-focus-visible-added]:not(.is-disabled) > &&, + .is-active:not(.is-disabled) > && { opacity: 1; } `; diff --git a/src/Pill/index.js b/src/Pill/index.js index 38794804..1f96e264 100644 --- a/src/Pill/index.js +++ b/src/Pill/index.js @@ -136,12 +136,12 @@ const HoverShade = styled.span` will-change: opacity; /* prettier-ignore */ - ${Wrapper}:not(.is-disabled):hover > & { + ${Wrapper}:not(.is-disabled):hover > && { opacity: 1; transition-duration: 50ms; } /* prettier-ignore */ - ${Wrapper}:not(.is-disabled):active > & { + ${Wrapper}:not(.is-disabled):active > && { opacity: 0; transition-duration: 250ms; } @@ -160,7 +160,7 @@ const FocusRing = styled.span.withConfig({ transition: opacity 250ms linear; will-change: opacity; - ${Wrapper}.focus-visible > & { + ${Wrapper}.focus-visible > && { top: -${_3px}; left: -${_3px}; bottom: -${_3px}; @@ -173,7 +173,7 @@ const FocusRing = styled.span.withConfig({ } /* prettier-ignore */ - ${Wrapper}:not(.is-disabled):active > & { + ${Wrapper}:not(.is-disabled):active > && { opacity: 1; transition-duration: 50ms; } diff --git a/src/Table/shared/RowSelectButton.js b/src/Table/shared/RowSelectButton.js index e0ea1b50..56ab1b20 100644 --- a/src/Table/shared/RowSelectButton.js +++ b/src/Table/shared/RowSelectButton.js @@ -17,11 +17,11 @@ const ButtonWrapper = styled(ButtonCore)` outline: none; } - tr[aria-selected='true'] & { + tr[aria-selected='true'] && { opacity: 1; } - tr[data-highlighted] &.focus-visible { + tr[data-highlighted] &&[data-focus-visible-added] { opacity: 1; } `; diff --git a/src/Table/shared/TableColumnHeader/ClickableHeader.js b/src/Table/shared/TableColumnHeader/ClickableHeader.js index 881820ea..c08b5e8d 100644 --- a/src/Table/shared/TableColumnHeader/ClickableHeader.js +++ b/src/Table/shared/TableColumnHeader/ClickableHeader.js @@ -59,8 +59,8 @@ const StyledSortArrow = styled(ArrowIcon).withConfig({shouldForwardProp})` opacity: 0; `} - ${ClickabilityIndicator}:hover &, - ${ClickabilityIndicator}.focus-visible & { + ${ClickabilityIndicator}:hover &&, + ${ClickabilityIndicator}.focus-visible && { fill: ${p => p.theme.links}; opacity: 1; transition-delay: 0.15s; diff --git a/src/Table/shared/TableRowHeader.js b/src/Table/shared/TableRowHeader.js index 7f4aebc5..7ca7cd0e 100644 --- a/src/Table/shared/TableRowHeader.js +++ b/src/Table/shared/TableRowHeader.js @@ -35,13 +35,13 @@ const Th = withTableContext(styled.th.withConfig({ border-bottom: ${p => borderValue(p.theme)}; ${headerBackgroundColor} - tr[aria-selected] & { + tr[aria-selected] && { padding-left: ${getCheckboxColumnWidth}; } /* Highlight SELECTABLE table rows */ - tr[data-highlighted] &, - tr[data-highlighted]:focus-within & { + tr[data-highlighted] &&, + tr[data-highlighted]:focus-within && { background-color: ${p => alpha( p.theme.links, @@ -49,8 +49,8 @@ const Th = withTableContext(styled.th.withConfig({ )}; } - tr[aria-selected='true'] &, - tr[aria-selected='true'][data-highlighted] & { + tr[aria-selected='true'] &&, + tr[aria-selected='true'][data-highlighted] && { background-color: ${p => alpha(p.theme.links, Math.max(0.15, +p.theme.shadeStrength))}; } From eadb184cb95e4d0be31ee15a3e65527f6202a813 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Tue, 9 Nov 2021 16:07:09 +0100 Subject: [PATCH 20/30] feat(Switch): More consistent and visible focus indicator, noissue --- src/Switch/index.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Switch/index.js b/src/Switch/index.js index 79f606fb..53865441 100644 --- a/src/Switch/index.js +++ b/src/Switch/index.js @@ -82,6 +82,27 @@ const SwitchIcon = styled(OkIcon)` `} `; +const _3px = pxToRem(3); + +const FocusRing = styled.span` + opacity: 0; + + input.focus-visible ~ & { + position: absolute; + top: -${_3px}; + left: -${_3px}; + bottom: -${_3px}; + right: -${_3px}; + + border-radius: inherit; + + box-shadow: 0 0 0 ${_3px} ${p => p.theme.links}; + + opacity: 1; + transition: opacity 100ms linear; + } +`; + const Switch = forwardRef( ({checked, disabled, id, tabIndex, ...otherProps}, ref) => { const inputRef = useRef(); @@ -106,6 +127,7 @@ const Switch = forwardRef( + ); } From 02d147c20fadd308d5540264523d51908beceef8 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Tue, 9 Nov 2021 15:14:26 +0000 Subject: [PATCH 21/30] chore(release): 14.1.0 [skip ci] # [14.1.0](https://github.com/5app/base5-ui/compare/v14.0.1...v14.1.0) (2021-11-09) ### Bug Fixes * Work around styles-components parent selector issue, noissue ([75247dc](https://github.com/5app/base5-ui/commit/75247dc632530db2c07cbdce7aac20fa097c216e)), closes [#3265](https://github.com/5app/base5-ui/issues/3265) ### Features * **Switch:** More consistent and visible focus indicator, noissue ([eadb184](https://github.com/5app/base5-ui/commit/eadb184cb95e4d0be31ee15a3e65527f6202a813)) --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0969a90c..63feaccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [14.1.0](https://github.com/5app/base5-ui/compare/v14.0.1...v14.1.0) (2021-11-09) + + +### Bug Fixes + +* Work around styles-components parent selector issue, noissue ([75247dc](https://github.com/5app/base5-ui/commit/75247dc632530db2c07cbdce7aac20fa097c216e)), closes [#3265](https://github.com/5app/base5-ui/issues/3265) + + +### Features + +* **Switch:** More consistent and visible focus indicator, noissue ([eadb184](https://github.com/5app/base5-ui/commit/eadb184cb95e4d0be31ee15a3e65527f6202a813)) + ## [14.0.1](https://github.com/5app/base5-ui/compare/v14.0.0...v14.0.1) (2021-11-05) diff --git a/package.json b/package.json index fdb83b87..c7a88f8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "14.0.1", + "version": "14.1.0", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 4200de32638c4aa1683344959bccacad719977ab Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Wed, 10 Nov 2021 11:48:50 +0100 Subject: [PATCH 22/30] feat(useBackLink): Accept array of basePaths, #164 --- src/useBackLink/README.stories.mdx | 7 +++++-- src/useBackLink/index.ts | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/useBackLink/README.stories.mdx b/src/useBackLink/README.stories.mdx index 524883cf..e19e253c 100644 --- a/src/useBackLink/README.stories.mdx +++ b/src/useBackLink/README.stories.mdx @@ -102,12 +102,15 @@ function ProductsPage() { } ``` -There's also an option for more complex cases that can't be solved by a static base path. `shouldSkipLocation` is a function that's called for any "back link candidate", and if you return `true` from it, that candidate will be skipped, and an earlier history entry (or the fallback) will be returned instead. +The `basePath` option also accepts an array of paths if there is more than one possible subroute. + +There's an additional option for more complex cases that can't be solved by a static base path. `shouldSkipLocation` is a function that's called for any "back link candidate", and if you return `true` from it, that candidate will be skipped, and an earlier history entry (or the fallback) will be returned instead. ```jsx const backLink = useBackLink({ fallback: '/home', - shouldSkipLocation: location => location.startsWith('/products'), + basePath: ['/products', '/article'], + shouldSkipLocation: location => location.state.quickview, }); ``` diff --git a/src/useBackLink/index.ts b/src/useBackLink/index.ts index e8fa3554..ff1552d9 100644 --- a/src/useBackLink/index.ts +++ b/src/useBackLink/index.ts @@ -5,7 +5,7 @@ import {stripSlashFromEnd} from '../utils'; interface BackLinkOptions { fallback?: Location; - basePath?: string; + basePath?: string | string[]; shouldSkipLocation?: (location: Location) => boolean; } @@ -23,7 +23,9 @@ function useBackLink(options: BackLinkOptions = {}): Location | null { ? stripSlashFromEnd(getLocationPathname(location)) : null; - const ignoreSubpath = formattedLocation?.startsWith(basePath); + const ignoreSubpath = Array.isArray(basePath) + ? basePath.some(path => formattedLocation?.startsWith(path)) + : formattedLocation?.startsWith(basePath); const skipLocation = shouldSkipLocation?.(location); From 75e3904e37c0814c9c07de21b30bd2697aef5bf4 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Wed, 10 Nov 2021 10:51:30 +0000 Subject: [PATCH 23/30] chore(release): 14.2.0 [skip ci] # [14.2.0](https://github.com/5app/base5-ui/compare/v14.1.0...v14.2.0) (2021-11-10) ### Features * **useBackLink:** Accept array of basePaths, [#164](https://github.com/5app/base5-ui/issues/164) ([4200de3](https://github.com/5app/base5-ui/commit/4200de32638c4aa1683344959bccacad719977ab)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63feaccc..369a2e2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [14.2.0](https://github.com/5app/base5-ui/compare/v14.1.0...v14.2.0) (2021-11-10) + + +### Features + +* **useBackLink:** Accept array of basePaths, [#164](https://github.com/5app/base5-ui/issues/164) ([4200de3](https://github.com/5app/base5-ui/commit/4200de32638c4aa1683344959bccacad719977ab)) + # [14.1.0](https://github.com/5app/base5-ui/compare/v14.0.1...v14.1.0) (2021-11-09) diff --git a/package.json b/package.json index c7a88f8f..bd26a565 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "14.1.0", + "version": "14.2.0", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From caeeb0af40d5eba87c2d4d521e8b247639575258 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Thu, 11 Nov 2021 17:55:32 +0100 Subject: [PATCH 24/30] feat(icons): New icons 'section' and 'trash', noissue --- src-icons/svg/favourite.svg | 4 ++- src-icons/svg/forbidden.svg | 4 +++ src-icons/svg/section.svg | 5 +++ src-icons/svg/{collection.svg => topic.svg} | 0 src-icons/svg/trash.svg | 3 ++ src/Icon/iconMap.js | 5 ++- src/docs/3-icons.stories.mdx | 5 ++- src/icons/Favourite.js | 2 +- src/icons/Forbidden.js | 38 +++++++++++++++++++++ src/icons/Section.js | 38 +++++++++++++++++++++ src/icons/{Collection.js => Topic.js} | 0 src/icons/Trash.js | 38 +++++++++++++++++++++ src/icons/index.js | 5 ++- 13 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src-icons/svg/forbidden.svg create mode 100644 src-icons/svg/section.svg rename src-icons/svg/{collection.svg => topic.svg} (100%) create mode 100644 src-icons/svg/trash.svg create mode 100644 src/icons/Forbidden.js create mode 100644 src/icons/Section.js rename src/icons/{Collection.js => Topic.js} (100%) create mode 100644 src/icons/Trash.js diff --git a/src-icons/svg/favourite.svg b/src-icons/svg/favourite.svg index 8e685da2..24cafd89 100644 --- a/src-icons/svg/favourite.svg +++ b/src-icons/svg/favourite.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/src-icons/svg/forbidden.svg b/src-icons/svg/forbidden.svg new file mode 100644 index 00000000..88161975 --- /dev/null +++ b/src-icons/svg/forbidden.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src-icons/svg/section.svg b/src-icons/svg/section.svg new file mode 100644 index 00000000..79520157 --- /dev/null +++ b/src-icons/svg/section.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src-icons/svg/collection.svg b/src-icons/svg/topic.svg similarity index 100% rename from src-icons/svg/collection.svg rename to src-icons/svg/topic.svg diff --git a/src-icons/svg/trash.svg b/src-icons/svg/trash.svg new file mode 100644 index 00000000..11d15b2e --- /dev/null +++ b/src-icons/svg/trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Icon/iconMap.js b/src/Icon/iconMap.js index 386e50da..078254c8 100644 --- a/src/Icon/iconMap.js +++ b/src/Icon/iconMap.js @@ -24,7 +24,6 @@ const iconMap = { captions: require('../icons/Captions').default, chat: require('../icons/Chat').default, chevron: require('../icons/Chevron').default, - collection: require('../icons/Collection').default, cycle: require('../icons/Cycle').default, dislike: require('../icons/Dislike').default, disk: require('../icons/Disk').default, @@ -38,6 +37,7 @@ const iconMap = { 'eye-open': require('../icons/EyeOpen').default, favourite: require('../icons/Favourite').default, flag: require('../icons/Flag').default, + forbidden: require('../icons/Forbidden').default, fullscreen: require('../icons/Fullscreen').default, 'fullscreen-exit': require('../icons/FullscreenExit').default, globe: require('../icons/Globe').default, @@ -62,6 +62,7 @@ const iconMap = { playlist: require('../icons/Playlist').default, 'playlist-link': require('../icons/PlaylistLink').default, plus: require('../icons/Plus').default, + section: require('../icons/Section').default, search: require('../icons/Search').default, send: require('../icons/Send').default, settings: require('../icons/Settings').default, @@ -74,6 +75,8 @@ const iconMap = { team: require('../icons/Team').default, time: require('../icons/Time').default, tools: require('../icons/Tools').default, + topic: require('../icons/Topic').default, + trash: require('../icons/Trash').default, trending: require('../icons/Trending').default, undo: require('../icons/Undo').default, unpin: require('../icons/Unpin').default, diff --git a/src/docs/3-icons.stories.mdx b/src/docs/3-icons.stories.mdx index 1d8fc7bc..c4d7bb98 100644 --- a/src/docs/3-icons.stories.mdx +++ b/src/docs/3-icons.stories.mdx @@ -36,7 +36,6 @@ import Icon from '../Icon'; - @@ -50,6 +49,7 @@ import Icon from '../Icon'; + @@ -75,6 +75,7 @@ import Icon from '../Icon'; + @@ -86,7 +87,9 @@ import Icon from '../Icon'; + + diff --git a/src/icons/Favourite.js b/src/icons/Favourite.js index 74be03f7..fb568310 100644 --- a/src/icons/Favourite.js +++ b/src/icons/Favourite.js @@ -23,7 +23,7 @@ const FavouriteIcon = forwardRef((props, ref) => { focusable="false" aria-hidden={ariaHidden} > - + ); }); diff --git a/src/icons/Forbidden.js b/src/icons/Forbidden.js new file mode 100644 index 00000000..766f3277 --- /dev/null +++ b/src/icons/Forbidden.js @@ -0,0 +1,38 @@ +import React, {forwardRef} from 'react'; + +import Svg from './BaseSvg'; + +const GandalfIcon = forwardRef((props, ref) => { + const {size, color, ...otherProps} = props; + + // Unless the icon has an explicit ARIA label, we'll hide it visually + const ariaHidden = + !(otherProps['aria-label'] || otherProps['aria-labelledby']) || + undefined; + + return ( + + + + ); +}); + +GandalfIcon.displayName = 'GandalfIcon'; + +GandalfIcon.defaultProps = { + size: 18, + color: 'currentcolor', +}; + +export default GandalfIcon; diff --git a/src/icons/Section.js b/src/icons/Section.js new file mode 100644 index 00000000..acc72738 --- /dev/null +++ b/src/icons/Section.js @@ -0,0 +1,38 @@ +import React, {forwardRef} from 'react'; + +import Svg from './BaseSvg'; + +const SectionIcon = forwardRef((props, ref) => { + const {size, color, ...otherProps} = props; + + // Unless the icon has an explicit ARIA label, we'll hide it visually + const ariaHidden = + !(otherProps['aria-label'] || otherProps['aria-labelledby']) || + undefined; + + return ( + + + + ); +}); + +SectionIcon.displayName = 'SectionIcon'; + +SectionIcon.defaultProps = { + size: 18, + color: 'currentcolor', +}; + +export default SectionIcon; diff --git a/src/icons/Collection.js b/src/icons/Topic.js similarity index 100% rename from src/icons/Collection.js rename to src/icons/Topic.js diff --git a/src/icons/Trash.js b/src/icons/Trash.js new file mode 100644 index 00000000..5c60dcb7 --- /dev/null +++ b/src/icons/Trash.js @@ -0,0 +1,38 @@ +import React, {forwardRef} from 'react'; + +import Svg from './BaseSvg'; + +const TrashIcon = forwardRef((props, ref) => { + const {size, color, ...otherProps} = props; + + // Unless the icon has an explicit ARIA label, we'll hide it visually + const ariaHidden = + !(otherProps['aria-label'] || otherProps['aria-labelledby']) || + undefined; + + return ( + + + + ); +}); + +TrashIcon.displayName = 'TrashIcon'; + +TrashIcon.defaultProps = { + size: 18, + color: 'currentcolor', +}; + +export default TrashIcon; diff --git a/src/icons/index.js b/src/icons/index.js index 0d4b8215..8b4fb791 100644 --- a/src/icons/index.js +++ b/src/icons/index.js @@ -22,7 +22,6 @@ export {default as Calendar} from './Calendar'; export {default as Captions} from './Captions'; export {default as Chat} from './Chat'; export {default as Chevron} from './Chevron'; -export {default as Collection} from './Collection'; export {default as Cycle} from './Cycle'; export {default as Dislike} from './Dislike'; export {default as Disk} from './Disk'; @@ -36,6 +35,7 @@ export {default as EyeClosed} from './EyeClosed'; export {default as EyeOpen} from './EyeOpen'; export {default as Favourite} from './Favourite'; export {default as Flag} from './Flag'; +export {default as Forbidden} from './Forbidden'; export {default as Fullscreen} from './Fullscreen'; export {default as FullscreenExit} from './FullscreenExit'; export {default as Globe} from './Globe'; @@ -60,6 +60,7 @@ export {default as Play} from './Play'; export {default as Playlist} from './Playlist'; export {default as PlaylistLink} from './PlaylistLink'; export {default as Plus} from './Plus'; +export {default as Section} from './Section'; export {default as Search} from './Search'; export {default as Send} from './Send'; export {default as Settings} from './Settings'; @@ -72,6 +73,8 @@ export {default as Tag} from './Tag'; export {default as Team} from './Team'; export {default as Time} from './Time'; export {default as Tools} from './Tools'; +export {default as Topic} from './Topic'; +export {default as Trash} from './Trash'; export {default as Trending} from './Trending'; export {default as Undo} from './Undo'; export {default as Unpin} from './Unpin'; From ad41f5567f585e420c2ad080ebc2184faac2790b Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Thu, 11 Nov 2021 16:59:28 +0000 Subject: [PATCH 25/30] chore(release): 14.3.0 [skip ci] # [14.3.0](https://github.com/5app/base5-ui/compare/v14.2.0...v14.3.0) (2021-11-11) ### Features * **icons:** New icons 'section' and 'trash', noissue ([caeeb0a](https://github.com/5app/base5-ui/commit/caeeb0af40d5eba87c2d4d521e8b247639575258)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 369a2e2a..555c461c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [14.3.0](https://github.com/5app/base5-ui/compare/v14.2.0...v14.3.0) (2021-11-11) + + +### Features + +* **icons:** New icons 'section' and 'trash', noissue ([caeeb0a](https://github.com/5app/base5-ui/commit/caeeb0af40d5eba87c2d4d521e8b247639575258)) + # [14.2.0](https://github.com/5app/base5-ui/compare/v14.1.0...v14.2.0) (2021-11-10) diff --git a/package.json b/package.json index bd26a565..353835d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "14.2.0", + "version": "14.3.0", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 3541f58381f652460351151d6873fa9e95013e28 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Tue, 16 Nov 2021 15:14:58 +0100 Subject: [PATCH 26/30] refactor(ThemeSection): Improve bg image handling, noissue BREAKING CHANGE: Removes the prop, just use now. Renames to . --- src/Pill/index.js | 2 +- src/ThemeSection/README.stories.mdx | 60 ++++++++++++---- src/ThemeSection/getReadableColorblock.js | 23 ------ src/ThemeSection/getThemeSection.ts | 88 +++++++++++++++++++++++ src/ThemeSection/index.tsx | 53 ++++++-------- src/theme/types.d.ts | 13 ++-- 6 files changed, 163 insertions(+), 76 deletions(-) delete mode 100644 src/ThemeSection/getReadableColorblock.js create mode 100644 src/ThemeSection/getThemeSection.ts diff --git a/src/Pill/index.js b/src/Pill/index.js index 1f96e264..3939a4b2 100644 --- a/src/Pill/index.js +++ b/src/Pill/index.js @@ -234,7 +234,7 @@ function defaultIconRenderer({iconName, iconColor}) { } return ( - + diff --git a/src/ThemeSection/README.stories.mdx b/src/ThemeSection/README.stories.mdx index 2fcfb06b..e179a976 100644 --- a/src/ThemeSection/README.stories.mdx +++ b/src/ThemeSection/README.stories.mdx @@ -1,6 +1,7 @@ import {Meta, Story, Canvas, ArgsTable} from '@storybook/addon-docs/blocks'; import Box from '../Box'; +import TextLink from '../TextLink'; import ThemeSection from './'; -# ThemeSection +# ThemeSection and ColorBlock ```jsx import ThemeSection from 'base5-ui/ThemeSection'; @@ -34,21 +33,54 @@ import ThemeSection from 'base5-ui/ThemeSection'; The `ThemeSection` component has two main functions: - When used at the top of the app, it provides your app with a global theme object via the `baseTheme` prop -- When used throughout the app, it assigns "theme sections" from your global theme to all children of the component. +- When used throughout the app, it defines a "theme section" from your global theme for all children of the component. -To assign a theme section, set the `name` prop to a valid key from `theme.sections`. Any components inside of a ThemeSection will then use the colours defined for this section, until overwritten by a more deeply nested ThemeSection. +To assign a theme section, set the `name` prop to a valid key from `theme.sections` or `theme.globals.colorBlocks`. Any components inside of a ThemeSection will then use the colours defined for this section, until overwritten by a more deeply nested `ThemeSection`. -You can also select a "color block" from the theme, as defined in `theme.globals.colorBlocks`. Color blocks are single colour values mapped to semantic names, e.g. `positive: 'green', negative: 'red'`. The mapped colour will be used as the background colour of the theme section, while the text and link colours will be automatically computed to be as contrasting/readable as possible. +Note that the `ThemeSection` component by itself will not apply any colours to its contents – it's up to nested components to pick and use colours from the Theme Section as needed. For example, the ThemedBox component combines a ThemeSection with a Box component to render an element with the defined text and background colour. -## Examples +## Example - + {props => ( - - This is a simple box picking up its parent ThemeSection's - text and background colours. + + {!props.withBackgroundImage && ( + + This is a simple box picking up its parent + ThemeSection's text and background colours.{' '} + Example link + + )} + +

This is a shaded area within a theme section.

+ {props.withBackgroundImage && ( +

+ When using a background image, make sure to + always place text or interactive content on a + shaded background. +

+ )} +
)} diff --git a/src/ThemeSection/getReadableColorblock.js b/src/ThemeSection/getReadableColorblock.js deleted file mode 100644 index f1ee102f..00000000 --- a/src/ThemeSection/getReadableColorblock.js +++ /dev/null @@ -1,23 +0,0 @@ -import {isValidColor, contrast, getColorBlock} from '../utils/colors'; - -function getReadableColorblock(colorBlock, theme, hasBgImage) { - if (!colorBlock) { - return undefined; - } - - const background = getColorBlock(colorBlock, theme); - - if (!isValidColor(background)) { - return undefined; - } - - const contrastingTextColor = hasBgImage ? 'white' : contrast(background); - - return { - text: contrastingTextColor, - links: contrastingTextColor, - background, - }; -} - -export default getReadableColorblock; diff --git a/src/ThemeSection/getThemeSection.ts b/src/ThemeSection/getThemeSection.ts new file mode 100644 index 00000000..532c5eef --- /dev/null +++ b/src/ThemeSection/getThemeSection.ts @@ -0,0 +1,88 @@ +import {isValidColor, isDark, getColorBlock} from '../utils/colors'; +import {ConfigThemeSection, LocalThemeSection} from '../theme/types'; + +/** + * `getThemeSection` covers three main cases: + * + * 1. When the `name` it's called with is the name of a theme section + * (as defined in theme.sections), it will simply return that section + * object. + * + * 2. When `name` is the name of a colorBlock (as defined in + * theme.globals.colorBlocks), a new section object is generated + * based on the colorBlock's color value, which will be used as the + * section's background colour. Contrasting foreground colours will be + * generated. + * + * 3. When the `withBackgroundImage` option is enabled, a new section object + * is generated that's specifically geared for ensuring readability of text + * on background images. With this option enabled, only the `background` + * will be based on the passed-in `name` property (i.e. the theme section + * or color block), all other values will be hard-coded. + */ + +interface SectionGetterOptions { + theme: LocalThemeSection; + name: string; + withBackgroundImage?: boolean; +} + +function getThemeSection({ + theme, + name, + withBackgroundImage, +}: SectionGetterOptions): ConfigThemeSection { + // If the passed in name isn't the name of a section, + // we treat it as a "color block" + const isThemeSection = Boolean(theme.sections?.[name]); + + const localThemeSection = isThemeSection + ? theme.sections[name] + : theme.sections[theme.currentThemeSectionName]; + + if (withBackgroundImage) { + let background = localThemeSection.background; + if (!isThemeSection) { + const colorBlockBackground = getColorBlock(name, theme); + if (isValidColor(colorBlockBackground)) { + background = colorBlockBackground; + } + } + return { + ...localThemeSection, + text: 'white', + links: 'white', + background, + shade: 'black', + textDimStrength: 0.8, + shadeStrength: 0.6, + lineStrength: 0.8, + }; + } + + if (isThemeSection) { + return localThemeSection; + } + + // If we haven't returned yet, this must be a color block + // and we need to dynamically construct a readable section + const background = getColorBlock(name, theme); + + if (!isValidColor(background)) { + return localThemeSection; + } + + const [contrastingText, contrastingShade] = isDark(background) + ? ['white', 'black'] + : ['black', 'white']; + + return { + ...localThemeSection, + text: contrastingText, + links: contrastingText, + shade: contrastingShade, + background, + }; +} + +export default getThemeSection; diff --git a/src/ThemeSection/index.tsx b/src/ThemeSection/index.tsx index 1fa14b3a..2f4f36e5 100644 --- a/src/ThemeSection/index.tsx +++ b/src/ThemeSection/index.tsx @@ -1,64 +1,55 @@ import React, {useCallback} from 'react'; import {ThemeProvider} from 'styled-components'; -import ThemeSectionError from './error'; -import getReadableColorblock from './getReadableColorblock'; +import ThemeSectionError from './error'; +import getThemeSection from './getThemeSection'; import {ThemeConfig, LocalThemeSection} from '../theme/types'; +type ThemeSectionName = string; + interface ThemeSectionProps { + /** + * The key (name) of the theme section, as defined in `baseTheme.sections`, + * or a color block from e.g. `baseTheme.globals.colorBlocks` + */ + name: ThemeSectionName; /** * Theme object to use as the base for all nested * ThemeSections. Usually this prop only needs to be * used once per app, to define its global theme. */ - baseTheme: ThemeConfig; - /** - * The key (name) of the theme section, as defined in `baseTheme.sections` - */ - name: string; - /** - * The key (name) of a colorBlock that you want to use - * to define foreground and background colours for the section. - * Your colorBlock definitions must be located at `baseTheme.globals.colorBlock` - */ - colorBlock?: string; + baseTheme?: ThemeConfig; /** - * Enable this prop when defining a color block with a background - * image. This will force its text colour to white, instead of a - * calculated colour that contrasts with the color block (as that - * colour will be covered by the image). - * Make sure to give your images a dark wash or gradient to ensure - * readability + * Enable this to make theme section properties safe for use with a + * background image. It will set text and link colours to white, and shade + * to black, with a high shadeStrength value. */ - hasBgImage: boolean; + withBackgroundImage?: boolean; children: React.ReactNode; } function ThemeSection(props: ThemeSectionProps): React.ReactElement { - const {baseTheme, children, colorBlock, hasBgImage, name} = props; + const {name, baseTheme, withBackgroundImage, children} = props; const constructLocalTheme = useCallback( (parentTheme = baseTheme): LocalThemeSection => { - const usedName = colorBlock ? 'colorBlock' : name; - const localThemeSection = parentTheme.sections[usedName]; + const localThemeSection = getThemeSection({ + theme: parentTheme, + name, + withBackgroundImage, + }); const parentThemeSectionName = parentTheme.currentThemeSectionName; - const colorBlockOverrides = getReadableColorblock( - colorBlock, - parentTheme, - hasBgImage - ); return { ...parentTheme, ...localThemeSection, - ...colorBlockOverrides, - currentThemeSectionName: usedName, + currentThemeSectionName: name, parent: parentTheme.sections[parentThemeSectionName], parentThemeSectionName, }; }, - [baseTheme, colorBlock, hasBgImage, name] + [baseTheme, name, withBackgroundImage] ); return ( diff --git a/src/theme/types.d.ts b/src/theme/types.d.ts index 3f9dcb4d..b72ac752 100644 --- a/src/theme/types.d.ts +++ b/src/theme/types.d.ts @@ -63,14 +63,13 @@ export interface ConfigThemeSection { readonly lineStrength: OpacityValue; } -export interface LocalThemeSection extends ConfigThemeSection { - globals: ThemeGlobals; - currentThemeSectionName?: string; - parentThemeSectionName?: string; - parent?: ConfigThemeSection; +export interface LocalThemeSection extends ThemeConfig, ConfigThemeSection { + readonly currentThemeSectionName?: string; + readonly parentThemeSectionName?: string; + readonly parent?: ConfigThemeSection; } export interface StyledComponentProps { - theme: LocalThemeSection; - [prop: string]: unknown; + readonly theme: LocalThemeSection; + readonly [prop: string]: unknown; } From cf5c3075babfc394daf98c2ecf747a8ca322a2db Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Tue, 16 Nov 2021 14:17:27 +0000 Subject: [PATCH 27/30] chore(release): 14.3.1 [skip ci] ## [14.3.1](https://github.com/5app/base5-ui/compare/v14.3.0...v14.3.1) (2021-11-16) ### Code Refactoring * **ThemeSection:** Improve bg image handling, noissue ([3541f58](https://github.com/5app/base5-ui/commit/3541f58381f652460351151d6873fa9e95013e28)) ### BREAKING CHANGES * **ThemeSection:** Removes the prop, just use now. Renames to . --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 555c461c..4e9929d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [14.3.1](https://github.com/5app/base5-ui/compare/v14.3.0...v14.3.1) (2021-11-16) + + +### Code Refactoring + +* **ThemeSection:** Improve bg image handling, noissue ([3541f58](https://github.com/5app/base5-ui/commit/3541f58381f652460351151d6873fa9e95013e28)) + + +### BREAKING CHANGES + +* **ThemeSection:** Removes the prop, just use now. +Renames to . + # [14.3.0](https://github.com/5app/base5-ui/compare/v14.2.0...v14.3.0) (2021-11-11) diff --git a/package.json b/package.json index 353835d6..8f2cd81d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "14.3.0", + "version": "14.3.1", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 051e1c3a808ca33aad3810fc731debfefc2e9584 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Tue, 16 Nov 2021 16:01:44 +0100 Subject: [PATCH 28/30] fix(ThemeSection): Add fallback for localThemeSection, noissue --- src/ThemeSection/getThemeSection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ThemeSection/getThemeSection.ts b/src/ThemeSection/getThemeSection.ts index 532c5eef..72a6f650 100644 --- a/src/ThemeSection/getThemeSection.ts +++ b/src/ThemeSection/getThemeSection.ts @@ -41,7 +41,7 @@ function getThemeSection({ : theme.sections[theme.currentThemeSectionName]; if (withBackgroundImage) { - let background = localThemeSection.background; + let background = localThemeSection?.background || 'grey'; if (!isThemeSection) { const colorBlockBackground = getColorBlock(name, theme); if (isValidColor(colorBlockBackground)) { From b47a1cc5f7d70c494c0372a2a2ee386df5199970 Mon Sep 17 00:00:00 2001 From: "@5app-Machine" Date: Tue, 16 Nov 2021 15:05:57 +0000 Subject: [PATCH 29/30] chore(release): 14.3.2 [skip ci] ## [14.3.2](https://github.com/5app/base5-ui/compare/v14.3.1...v14.3.2) (2021-11-16) ### Bug Fixes * **ThemeSection:** Add fallback for localThemeSection, noissue ([051e1c3](https://github.com/5app/base5-ui/commit/051e1c3a808ca33aad3810fc731debfefc2e9584)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e9929d0..1cdcacbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [14.3.2](https://github.com/5app/base5-ui/compare/v14.3.1...v14.3.2) (2021-11-16) + + +### Bug Fixes + +* **ThemeSection:** Add fallback for localThemeSection, noissue ([051e1c3](https://github.com/5app/base5-ui/commit/051e1c3a808ca33aad3810fc731debfefc2e9584)) + ## [14.3.1](https://github.com/5app/base5-ui/compare/v14.3.0...v14.3.1) (2021-11-16) diff --git a/package.json b/package.json index 8f2cd81d..35862fae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "base5-ui", - "version": "14.3.1", + "version": "14.3.2", "description": "5app's reusable UI component library", "main": "index.js", "types": "dist/index.d.ts", From 4c11100ae351eef9ddd016012adf565e5389ce47 Mon Sep 17 00:00:00 2001 From: Dionysos Dajka Date: Tue, 16 Nov 2021 16:53:53 +0100 Subject: [PATCH 30/30] docs(ThemeSection): Fix heading, noissue --- src/ThemeSection/README.stories.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ThemeSection/README.stories.mdx b/src/ThemeSection/README.stories.mdx index e179a976..e7e6139b 100644 --- a/src/ThemeSection/README.stories.mdx +++ b/src/ThemeSection/README.stories.mdx @@ -24,7 +24,7 @@ import ThemeSection from './'; }} /> -# ThemeSection and ColorBlock +# ThemeSection ```jsx import ThemeSection from 'base5-ui/ThemeSection';