@kaizen/components 3.3.4 → 3.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/Select/Select.cjs +2 -1
- package/dist/cjs/src/Select/Select.module.scss.cjs +26 -26
- package/dist/cjs/src/TitleBlock/TitleBlock.cjs +20 -18
- package/dist/cjs/src/TitleBlock/TitleBlock.module.scss.cjs +54 -53
- package/dist/esm/src/Select/Select.mjs +2 -1
- package/dist/esm/src/Select/Select.module.scss.mjs +26 -26
- package/dist/esm/src/TitleBlock/TitleBlock.mjs +20 -18
- package/dist/esm/src/TitleBlock/TitleBlock.module.scss.mjs +54 -53
- package/dist/styles.css +2 -2
- package/dist/types/TitleBlock/TitleBlock.d.ts +1 -1
- package/dist/types/TitleBlock/types.d.ts +11 -0
- package/package.json +1 -1
- package/src/Select/Select.module.scss +12 -0
- package/src/Select/Select.tsx +1 -1
- package/src/TitleBlock/TitleBlock.module.scss +42 -0
- package/src/TitleBlock/TitleBlock.tsx +2 -0
- package/src/TitleBlock/_docs/TitleBlock--sticky-banner-guidelines.mdx +192 -0
- package/src/TitleBlock/_docs/TitleBlock.stories.tsx +240 -0
- package/src/TitleBlock/_docs/stickyBanner.module.css +44 -0
- package/src/TitleBlock/types.ts +11 -0
|
@@ -3,9 +3,11 @@ import { type Meta, type StoryObj } from '@storybook/react'
|
|
|
3
3
|
import { expect, waitFor, within } from '@storybook/test'
|
|
4
4
|
import { Heading } from 'react-aria-components'
|
|
5
5
|
import { Icon } from '~components/Icon'
|
|
6
|
+
import { GlobalNotification } from '~components/Notification'
|
|
6
7
|
import { assetUrl } from '~components/utils/hostedAssets'
|
|
7
8
|
import { StickerSheet } from '~storybook/components/StickerSheet'
|
|
8
9
|
import { NavigationTab, TitleBlock } from '../index'
|
|
10
|
+
import stickyBannerStyles from './stickyBanner.module.css'
|
|
9
11
|
|
|
10
12
|
const SECONDARY_ACTIONS = [
|
|
11
13
|
{
|
|
@@ -125,6 +127,50 @@ export default meta
|
|
|
125
127
|
|
|
126
128
|
type Story = StoryObj<typeof meta>
|
|
127
129
|
|
|
130
|
+
const STICKY_SCROLLABLE_CONTAINER_STYLES = {
|
|
131
|
+
height: '200px',
|
|
132
|
+
overflowY: 'auto' as const,
|
|
133
|
+
backgroundColor: 'var(--color-white)',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const STICKY_SCROLLABLE_FRAME_STYLES = {
|
|
137
|
+
margin: '0 auto',
|
|
138
|
+
maxWidth: '1200px',
|
|
139
|
+
border: '1px solid var(--border-solid-border-color)',
|
|
140
|
+
borderRadius: '12px',
|
|
141
|
+
overflow: 'hidden' as const,
|
|
142
|
+
backgroundColor: 'var(--color-white)',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const renderStickyScrollableTitleBlock = (
|
|
146
|
+
args: React.ComponentProps<typeof TitleBlock>,
|
|
147
|
+
): JSX.Element => (
|
|
148
|
+
<div style={STICKY_SCROLLABLE_FRAME_STYLES}>
|
|
149
|
+
<div
|
|
150
|
+
data-scroll-container="sticky-top-strip"
|
|
151
|
+
className={stickyBannerStyles.scrollContainer}
|
|
152
|
+
style={{
|
|
153
|
+
...STICKY_SCROLLABLE_CONTAINER_STYLES,
|
|
154
|
+
// Taller than the shared default so the fake nav and the TitleBlock
|
|
155
|
+
// content fit without cramping.
|
|
156
|
+
height: '400px',
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<div className={stickyBannerStyles.fakeAppChromeNav}>Fake app chrome nav (72px)</div>
|
|
160
|
+
|
|
161
|
+
<TitleBlock {...args} />
|
|
162
|
+
|
|
163
|
+
<div
|
|
164
|
+
style={{
|
|
165
|
+
// Taller than the container so it overflows and the sticky behaviour
|
|
166
|
+
// (+ the play() scroll assertion) stays exercised.
|
|
167
|
+
height: '500px',
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
|
|
128
174
|
export const Playground: Story = {
|
|
129
175
|
parameters: {
|
|
130
176
|
docs: {
|
|
@@ -692,3 +738,197 @@ export const WithOnlySecondaryActions: Story = {
|
|
|
692
738
|
avatar: undefined,
|
|
693
739
|
},
|
|
694
740
|
}
|
|
741
|
+
|
|
742
|
+
export const StickyTopStripInScrollableContainer: Story = {
|
|
743
|
+
name: 'Sticker Sheet (Sticky Top Strip In Scrollable Container)',
|
|
744
|
+
parameters: {
|
|
745
|
+
viewport: viewports,
|
|
746
|
+
chromatic: chromaticViewports,
|
|
747
|
+
},
|
|
748
|
+
args: {
|
|
749
|
+
sticky: true,
|
|
750
|
+
},
|
|
751
|
+
render: (args) => {
|
|
752
|
+
const { variant: _variant, ...argsWithoutVariant } = args
|
|
753
|
+
|
|
754
|
+
return (
|
|
755
|
+
<StickerSheet title="Sticky top strip within a scrollable container">
|
|
756
|
+
<StickerSheet.Row header="Default (Purple background)">
|
|
757
|
+
{renderStickyScrollableTitleBlock({
|
|
758
|
+
...argsWithoutVariant,
|
|
759
|
+
title: 'Default Variant',
|
|
760
|
+
subtitle: 'Sticky top strip inside a scrollable content area',
|
|
761
|
+
breadcrumb: {
|
|
762
|
+
path: '#',
|
|
763
|
+
text: 'Back to home',
|
|
764
|
+
},
|
|
765
|
+
navigationTabs: [
|
|
766
|
+
<NavigationTab key="1" text="Overview" href="#" active />,
|
|
767
|
+
<NavigationTab key="2" text="Settings" href="#" />,
|
|
768
|
+
],
|
|
769
|
+
})}
|
|
770
|
+
</StickerSheet.Row>
|
|
771
|
+
<StickerSheet.Row header="Education (Blue background)">
|
|
772
|
+
{renderStickyScrollableTitleBlock({
|
|
773
|
+
...argsWithoutVariant,
|
|
774
|
+
variant: 'education',
|
|
775
|
+
title: 'Education Variant',
|
|
776
|
+
subtitle: 'Sticky top strip inside a scrollable content area',
|
|
777
|
+
breadcrumb: {
|
|
778
|
+
path: '#',
|
|
779
|
+
text: 'Back to courses',
|
|
780
|
+
},
|
|
781
|
+
navigationTabs: [
|
|
782
|
+
<NavigationTab key="1" variant="education" text="Lessons" href="#" active />,
|
|
783
|
+
<NavigationTab key="2" variant="education" text="Assignments" href="#" />,
|
|
784
|
+
],
|
|
785
|
+
})}
|
|
786
|
+
</StickerSheet.Row>
|
|
787
|
+
<StickerSheet.Row header="Admin (White background)">
|
|
788
|
+
{renderStickyScrollableTitleBlock({
|
|
789
|
+
...argsWithoutVariant,
|
|
790
|
+
variant: 'admin',
|
|
791
|
+
title: 'Admin Variant',
|
|
792
|
+
subtitle: 'Sticky top strip inside a scrollable content area',
|
|
793
|
+
breadcrumb: {
|
|
794
|
+
path: '#',
|
|
795
|
+
text: 'Back to dashboard',
|
|
796
|
+
},
|
|
797
|
+
navigationTabs: [
|
|
798
|
+
<NavigationTab key="1" variant="admin" text="Users" href="#" active />,
|
|
799
|
+
<NavigationTab key="2" variant="admin" text="Settings" href="#" />,
|
|
800
|
+
],
|
|
801
|
+
})}
|
|
802
|
+
</StickerSheet.Row>
|
|
803
|
+
<StickerSheet.Row header="Light (White background)">
|
|
804
|
+
{renderStickyScrollableTitleBlock({
|
|
805
|
+
...argsWithoutVariant,
|
|
806
|
+
variant: 'light',
|
|
807
|
+
title: 'Light Variant',
|
|
808
|
+
subtitle: 'Sticky top strip inside a scrollable content area',
|
|
809
|
+
breadcrumb: {
|
|
810
|
+
path: '#',
|
|
811
|
+
text: 'Back to overview',
|
|
812
|
+
},
|
|
813
|
+
navigationTabs: [
|
|
814
|
+
<NavigationTab key="1" variant="light" text="Details" href="#" active />,
|
|
815
|
+
<NavigationTab key="2" variant="light" text="Analytics" href="#" />,
|
|
816
|
+
],
|
|
817
|
+
})}
|
|
818
|
+
</StickerSheet.Row>
|
|
819
|
+
</StickerSheet>
|
|
820
|
+
)
|
|
821
|
+
},
|
|
822
|
+
play: async ({ canvasElement, step }) => {
|
|
823
|
+
await step('scroll each sticky container before snapshot', async () => {
|
|
824
|
+
const scrollContainers = canvasElement.querySelectorAll<HTMLElement>(
|
|
825
|
+
'[data-scroll-container="sticky-top-strip"]',
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
scrollContainers.forEach((scrollContainer) => {
|
|
829
|
+
scrollContainer.scrollTo({
|
|
830
|
+
top: scrollContainer.scrollHeight - scrollContainer.clientHeight,
|
|
831
|
+
})
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
await waitFor(() => {
|
|
835
|
+
scrollContainers.forEach((scrollContainer) => {
|
|
836
|
+
expect(scrollContainer.scrollTop).toBeGreaterThan(0)
|
|
837
|
+
})
|
|
838
|
+
})
|
|
839
|
+
})
|
|
840
|
+
},
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Sticky banner (GlobalNotification) above a sticky TitleBlock.
|
|
845
|
+
*
|
|
846
|
+
* Follows the additive-offset pattern: the banner pins below the app-chrome
|
|
847
|
+
* bar by *reading* the shared `--app-chrome-sticky-offset` (never reassigning
|
|
848
|
+
* it), and the TitleBlock strip reserves space for the banner via the opt-in
|
|
849
|
+
* `--titleblock-sticky-offset`, set on a `display: contents` wrapper right
|
|
850
|
+
* around the TitleBlock. The banner height is measured with a ResizeObserver
|
|
851
|
+
* and fed into that variable so the strip stays flush at any banner height.
|
|
852
|
+
*/
|
|
853
|
+
const StickyBannerAboveTitleBlock = (
|
|
854
|
+
args: React.ComponentProps<typeof TitleBlock>,
|
|
855
|
+
): JSX.Element => {
|
|
856
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
857
|
+
const [bannerHeight, setBannerHeight] = React.useState(0)
|
|
858
|
+
|
|
859
|
+
React.useLayoutEffect(() => {
|
|
860
|
+
// GlobalNotification doesn't forward a ref, so grab its DOM node by the
|
|
861
|
+
// data attribute to measure the banner height.
|
|
862
|
+
const el = containerRef.current?.querySelector<HTMLElement>('[data-sticky-banner]')
|
|
863
|
+
if (!el) return
|
|
864
|
+
const update = (): void => setBannerHeight(el.offsetHeight)
|
|
865
|
+
update()
|
|
866
|
+
const observer = new ResizeObserver(update)
|
|
867
|
+
observer.observe(el)
|
|
868
|
+
return () => observer.disconnect()
|
|
869
|
+
}, [])
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<div
|
|
873
|
+
ref={containerRef}
|
|
874
|
+
data-scroll-container="sticky-top-strip"
|
|
875
|
+
className={stickyBannerStyles.scrollContainer}
|
|
876
|
+
style={{
|
|
877
|
+
height: '500px',
|
|
878
|
+
overflowY: 'auto',
|
|
879
|
+
}}
|
|
880
|
+
>
|
|
881
|
+
<div className={stickyBannerStyles.fakeAppChromeNav}>Fake app chrome nav (72px)</div>
|
|
882
|
+
<GlobalNotification
|
|
883
|
+
variant="informative"
|
|
884
|
+
persistent
|
|
885
|
+
data-sticky-banner
|
|
886
|
+
classNameOverride={stickyBannerStyles.stickyBanner}
|
|
887
|
+
>
|
|
888
|
+
This global notification renders directly above the TitleBlock.
|
|
889
|
+
</GlobalNotification>
|
|
890
|
+
<div
|
|
891
|
+
style={{
|
|
892
|
+
display: 'contents',
|
|
893
|
+
['--titleblock-sticky-offset' as string]: `${bannerHeight}px`,
|
|
894
|
+
}}
|
|
895
|
+
>
|
|
896
|
+
<TitleBlock {...args} />
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
{/* Taller than the container so it overflows and the sticky behaviour
|
|
900
|
+
(+ the play() scroll assertion) stays exercised. */}
|
|
901
|
+
<div style={{ height: '600px' }} />
|
|
902
|
+
</div>
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export const WithGlobalNotificationAbove: Story = {
|
|
907
|
+
parameters: {
|
|
908
|
+
viewport: viewports,
|
|
909
|
+
chromatic: chromaticViewports,
|
|
910
|
+
},
|
|
911
|
+
args: {
|
|
912
|
+
sticky: true,
|
|
913
|
+
},
|
|
914
|
+
render: (args) => <StickyBannerAboveTitleBlock {...args} />,
|
|
915
|
+
play: async ({ canvasElement, step }) => {
|
|
916
|
+
await step('scroll the sticky container before snapshot', async () => {
|
|
917
|
+
const scrollContainers = canvasElement.querySelectorAll<HTMLElement>(
|
|
918
|
+
'[data-scroll-container="sticky-top-strip"]',
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
scrollContainers.forEach((scrollContainer) => {
|
|
922
|
+
scrollContainer.scrollTo({
|
|
923
|
+
top: scrollContainer.scrollHeight - scrollContainer.clientHeight,
|
|
924
|
+
})
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
await waitFor(() => {
|
|
928
|
+
scrollContainers.forEach((scrollContainer) => {
|
|
929
|
+
expect(scrollContainer.scrollTop).toBeGreaterThan(0)
|
|
930
|
+
})
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
},
|
|
934
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Pin below the app-chrome bar by reading the shared offset (read-only). */
|
|
2
|
+
.stickyBanner {
|
|
3
|
+
position: sticky;
|
|
4
|
+
top: var(--app-chrome-sticky-offset, 72px);
|
|
5
|
+
z-index: 5;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Stand-in for the AppChrome content scroll area. Sets the shared
|
|
10
|
+
* `--app-chrome-sticky-offset` that the sticky titleRow and banner read.
|
|
11
|
+
* Below 1080px the AppChrome nav collapses to a hamburger (no persistent top
|
|
12
|
+
* bar), so the offset drops to 0 — matching kaizen's own
|
|
13
|
+
* `@media (width < 1080px)` rule on the titleRow.
|
|
14
|
+
*/
|
|
15
|
+
.scrollContainer {
|
|
16
|
+
--app-chrome-sticky-offset: 72px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Fake AppChrome top nav — a sticky bar at >=1080px, removed below that
|
|
21
|
+
* (the real nav becomes a hamburger with no top bar).
|
|
22
|
+
*/
|
|
23
|
+
.fakeAppChromeNav {
|
|
24
|
+
position: sticky;
|
|
25
|
+
top: 0;
|
|
26
|
+
z-index: 2;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
height: 72px;
|
|
30
|
+
padding-inline: 16px;
|
|
31
|
+
background-color: var(--color-purple-800);
|
|
32
|
+
color: var(--color-white);
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@media (width < 1080px) {
|
|
37
|
+
.scrollContainer {
|
|
38
|
+
--app-chrome-sticky-offset: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.fakeAppChromeNav {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/TitleBlock/types.ts
CHANGED
|
@@ -32,6 +32,17 @@ export type TitleBlockProps = {
|
|
|
32
32
|
secondaryOverflowMenuItems?: TitleBlockMenuItemProps[]
|
|
33
33
|
navigationTabs?: NavigationTabs
|
|
34
34
|
collapseNavigationAreaWhenPossible?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Makes the top strip stick to the top of its scrollable container as the
|
|
37
|
+
* content scrolls. The TitleBlock must be rendered inside a scrollable
|
|
38
|
+
* ancestor. When enabled the root becomes `display: contents` so the strip
|
|
39
|
+
* pins as a sibling of the scrolling content; its offset is
|
|
40
|
+
* `--app-chrome-sticky-offset` (owned by AppChrome, read-only) plus the
|
|
41
|
+
* opt-in `--titleblock-sticky-offset` a consumer sets to reserve space for a
|
|
42
|
+
* banner above it. Below 1080px the app-chrome offset is dropped to match the
|
|
43
|
+
* collapsed hamburger nav.
|
|
44
|
+
*/
|
|
45
|
+
sticky?: boolean
|
|
35
46
|
textDirection?: TextDirection
|
|
36
47
|
surveyStatus?: SurveyStatus
|
|
37
48
|
id?: string
|