-
Notifications
You must be signed in to change notification settings - Fork 274
[copilot] Front end build for new topic discovery component #13927
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b0d2b41
d7dc56d
9bdf7ea
038e00c
a61d00a
26e64ed
f054e9a
a0d2f4a
2d67986
ac0bfb6
e1b1a65
d0f3f31
8a9d605
f113ba2
fee5063
ab81a57
e7b34f3
c3bdeae
de52237
d0623e0
aa4a4af
2f9ca03
c383979
e8aeb96
2685537
b89e805
5cf98a3
be42f6b
233fef3
eb66fc8
f70e3be
364e96e
737aa63
37c3c3c
8d94629
72b7d2c
b258381
2546115
38ad740
c5c9f82
59902a5
e179633
2bf6d1d
52e4c43
72025cc
80e2236
44e303c
101cfda
d7adc3e
d2b3793
544ed41
700f779
c6d7aa0
1324f93
b66e5b8
d369955
8b16375
5f2b45c
b280357
503ef4b
bb9b360
91deeee
820d501
de36172
2b678c5
8f5d187
e3d9bd1
86009af
853929c
e5ca6a8
8ab1faf
2d4c94d
155dcab
1c781cf
e8dc816
e2ae9c7
cbe7fdd
e3626a2
248a77b
04f8f72
edef6b9
77e1379
9aa1ea2
f0cb6e3
bcdf678
be821bb
9642ea1
70bb0ad
4b3cd42
637af47
f6a38d5
83f96b0
39e4488
d1a5b57
b7e2c8c
6ac8e4c
75b4dce
119e67e
36f1014
96f93eb
85748af
e5e0c84
402091f
fc542d2
7329a2a
61c5c62
c6b9cc7
bd0de80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { css, Theme } from '@emotion/react'; | ||
|
|
||
| const styles = { | ||
| wrapper: ({ palette, spacings }: Theme) => | ||
| css({ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: `${spacings.HALF}rem`, | ||
| borderBottom: `1px solid ${palette.GREY_5}`, | ||
| }), | ||
|
|
||
| tabList: () => | ||
| css({ | ||
| display: 'flex', | ||
| overflowX: 'auto', | ||
| scrollBehavior: 'auto', | ||
| scrollbarWidth: 'none', | ||
| msOverflowStyle: 'none', | ||
| '&::-webkit-scrollbar': { | ||
| display: 'none', | ||
| }, | ||
| }), | ||
|
|
||
| tab: ({ palette, spacings, fontSizes, fontVariants }: Theme) => | ||
| css({ | ||
| ...fontVariants.sansRegular, | ||
| ...fontSizes.pica, | ||
| whiteSpace: 'nowrap', | ||
| background: 'none', | ||
| border: 'none', | ||
| borderBottom: '3px solid transparent', | ||
| padding: `${spacings.FULL}rem ${spacings.DOUBLE}rem`, | ||
| cursor: 'pointer', | ||
| color: palette.GREY_6, | ||
| '&:hover': { | ||
| color: palette.GREY_10, | ||
| borderBottomColor: palette.GREY_5, | ||
| }, | ||
| '&:focus-visible': { | ||
| outline: `3px solid ${palette.BLACK}`, | ||
| outlineOffset: '-3px', | ||
| }, | ||
| }), | ||
|
|
||
| tabActive: ({ palette }: Theme) => | ||
| css({ | ||
| color: palette.GREY_10, | ||
| borderBottomColor: palette.POSTBOX, | ||
| fontWeight: 700, | ||
| }), | ||
|
|
||
| scrollButton: ({ palette, spacings }: Theme) => | ||
| css({ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| flexShrink: 0, | ||
| background: 'none', | ||
| border: 'none', | ||
| cursor: 'pointer', | ||
| padding: `${spacings.HALF}rem`, | ||
| color: palette.GREY_6, | ||
| '& svg': { | ||
| width: `${spacings.DOUBLE}rem`, | ||
| height: `${spacings.DOUBLE}rem`, | ||
| fill: 'currentcolor', | ||
| }, | ||
| '&:hover:not(:disabled)': { | ||
| color: palette.GREY_10, | ||
| }, | ||
| '&:disabled': { | ||
| cursor: 'default', | ||
| color: '#8A8C8E', | ||
| }, | ||
| }), | ||
| }; | ||
|
|
||
| export default styles; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import { useCallback, useEffect, useRef, useState, use } from 'react'; | ||
| import { ServiceContext } from '#app/contexts/ServiceContext'; | ||
| import { Chevron, ChevronOrientation } from '#app/components/icons'; | ||
| import styles from './index.styles'; | ||
|
|
||
| type ScrollableTabsProps = { | ||
| tabs: { id: string; label: string }[]; | ||
| activeTabId: string; | ||
| onTabChange: (tabId: string) => void; | ||
| labelledBy: string; | ||
| }; | ||
|
|
||
| const ScrollableTabs = ({ | ||
| tabs, | ||
| activeTabId, | ||
| onTabChange, | ||
| labelledBy, | ||
| }: ScrollableTabsProps) => { | ||
| const { dir } = use(ServiceContext); | ||
| const tabListRef = useRef<HTMLDivElement>(null); | ||
| const [canScrollStart, setCanScrollStart] = useState(false); | ||
| const [canScrollEnd, setCanScrollEnd] = useState(false); | ||
|
|
||
| const checkOverflow = useCallback(() => { | ||
| const el = tabListRef.current; | ||
| if (!el) return; | ||
|
|
||
| const { scrollLeft, scrollWidth, clientWidth } = el; | ||
| const absScroll = Math.abs(scrollLeft); | ||
|
|
||
| setCanScrollStart(absScroll > 0); | ||
| setCanScrollEnd(absScroll + clientWidth + 1 < scrollWidth); | ||
| }, []); | ||
|
|
||
| useEffect(() => { | ||
| const el = tabListRef.current; | ||
| if (!el) return undefined; | ||
|
|
||
| el.addEventListener('scroll', checkOverflow); | ||
| checkOverflow(); | ||
|
|
||
| const resizeObserver = new ResizeObserver(checkOverflow); | ||
| resizeObserver.observe(el); | ||
|
|
||
| return () => { | ||
| el.removeEventListener('scroll', checkOverflow); | ||
| resizeObserver.disconnect(); | ||
| }; | ||
| }, [checkOverflow]); | ||
|
|
||
| const scroll = (direction: 'start' | 'end') => { | ||
| const el = tabListRef.current; | ||
| if (!el) return; | ||
|
|
||
| const scrollAmount = el.clientWidth * 0.75; | ||
| const isForward = | ||
| (direction === 'end' && dir === 'ltr') || | ||
| (direction === 'start' && dir === 'rtl'); | ||
|
|
||
| el.scrollBy({ | ||
| left: isForward ? scrollAmount : -scrollAmount, | ||
| behavior: 'smooth', | ||
| }); | ||
| }; | ||
|
|
||
| const handleKeyDown = (event: React.KeyboardEvent) => { | ||
| const currentIndex = tabs.findIndex(tab => tab.id === activeTabId); | ||
| let nextIndex: number | null = null; | ||
|
|
||
| if (event.key === 'ArrowRight') { | ||
| nextIndex = | ||
| dir === 'ltr' | ||
| ? (currentIndex + 1) % tabs.length | ||
| : (currentIndex - 1 + tabs.length) % tabs.length; | ||
| } else if (event.key === 'ArrowLeft') { | ||
| nextIndex = | ||
| dir === 'ltr' | ||
| ? (currentIndex - 1 + tabs.length) % tabs.length | ||
| : (currentIndex + 1) % tabs.length; | ||
| } else if (event.key === 'Home') { | ||
| nextIndex = 0; | ||
| } else if (event.key === 'End') { | ||
| nextIndex = tabs.length - 1; | ||
| } | ||
|
|
||
| if (nextIndex !== null) { | ||
| event.preventDefault(); | ||
| const nextTab = tabs[nextIndex]; | ||
| onTabChange(nextTab.id); | ||
|
|
||
| const tabEl = tabListRef.current?.querySelector( | ||
| `[data-tab-id="${CSS.escape(nextTab.id)}"]`, | ||
| ) as HTMLButtonElement | null; | ||
| tabEl?.focus(); | ||
| tabEl?.scrollIntoView({ block: 'nearest', inline: 'nearest' }); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div css={styles.wrapper}> | ||
| <button | ||
| type="button" | ||
| css={styles.scrollButton} | ||
| onClick={() => scroll('start')} | ||
| disabled={!canScrollStart} | ||
| aria-hidden="true" | ||
| tabIndex={-1} | ||
| data-testid="scroll-start" | ||
| > | ||
| <Chevron orientation={ChevronOrientation.BACKWARD} dir={dir} /> | ||
|
amoore108 marked this conversation as resolved.
Outdated
|
||
| </button> | ||
|
|
||
| <div | ||
| ref={tabListRef} | ||
| role="tablist" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More an a11y consideration here but this seems fine for mouse users, but have we checked if the UI is fully keyboard accessible pre-swarm since this is a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've had a test of it manually and the tab behaviour seems expected, but doesn't work using arrow keys. If that's something we would want (I'm unsure but I think that is expected for tab accessibility) we would likely need a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The arrow keys should be working with the tabs themself, but not with the promos. I just tested locally and seems arrows seem to work for the tabs.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @louisearchibald Yeah I think you're right. I’ve checked the implementation again and we currently have the tab roles/ |
||
| aria-labelledby={labelledBy} | ||
| css={styles.tabList} | ||
| onKeyDown={handleKeyDown} | ||
| tabIndex={0} | ||
|
amoore108 marked this conversation as resolved.
Outdated
|
||
| > | ||
| {tabs.map(tab => { | ||
| const isActive = tab.id === activeTabId; | ||
| return ( | ||
| <button | ||
| key={tab.id} | ||
| type="button" | ||
| role="tab" | ||
| id={`tab-${tab.id}`} | ||
| data-tab-id={tab.id} | ||
| aria-selected={isActive} | ||
| aria-controls={`tabpanel-${tab.id}`} | ||
| tabIndex={isActive ? 0 : -1} | ||
| css={[styles.tab, isActive && styles.tabActive]} | ||
|
amoore108 marked this conversation as resolved.
|
||
| onClick={() => onTabChange(tab.id)} | ||
| > | ||
| {tab.label} | ||
| </button> | ||
| ); | ||
| })} | ||
| </div> | ||
|
|
||
| <button | ||
| type="button" | ||
| css={styles.scrollButton} | ||
| onClick={() => scroll('end')} | ||
| disabled={!canScrollEnd} | ||
| aria-hidden="true" | ||
| tabIndex={-1} | ||
| data-testid="scroll-end" | ||
| > | ||
| <Chevron orientation={ChevronOrientation.FORWARD} dir={dir} /> | ||
| </button> | ||
|
amoore108 marked this conversation as resolved.
Outdated
|
||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ScrollableTabs; | ||
Uh oh!
There was an error while loading. Please reload this page.