diff --git a/docs/pages/kcard.vue b/docs/pages/kcard.vue index c2fdb8a66..93952847b 100644 --- a/docs/pages/kcard.vue +++ b/docs/pages/kcard.vue @@ -7,7 +7,10 @@

It manages the layout, including the thumbnail image, title, and other content. It offers several base layouts and many customization options. Cards like the examples shown can be created, and many others.

- + KCard has two orientations: horizontal and vertical. It is also possible to configure whether a thumbnail area is displayed, its size and alignment. By combining orientation, thumbnailDisplay and thumbnailAlign props, the following card layouts can be achieved to organize diverse kinds of content:

- + - + - + Use aboveTitle, belowTitle, and footer slots to add content to a card. KCard will organize these areas according to its . Apply custom styling to the inner content of slots to achieve desired effects.

- + Use the thumbnailPlaceholder slot to add a placeholder element, such as an icon, to this area. Provide a placeholder element even if a thumbnail image is available. This serves as a progressive loading experience where the placeholder element is displayed until the image is loaded, and is particularly important on slower networks.

- + This applies to all slot content, but considering accessibility is especially important with interactive elements. For instance, ariaLabel is applied to the bookmark icon button in the following example so that screenreaders can communicate its purpose. In production, more work would be needed to indicate the bookmark's toggled state. Always assess on a case-by-case basis.

- + Managing the selection state is not KCard's responsibility.

- + - + - + - + Setting height on cards is discouraged. Instead, manage height bottom-up, for example by setting height on card sections, using text truncation, or other ways to limit its inner content. Such approaches ensure content tolerance, prevent from unexpected overflows or excessive height, and keep vertical alignment of card sections consistent on a grid row. This is especially important when dealing with unknown lenghts or amounts of content displayed in cards. Consider:

- + Grid configuration can be combined with KCard's settings to further improve responsive experience. A common pattern is switching KCard's horizontal orientation to vertical for smaller screens to organize content more effectively in limited space:

- + This technique also works for adjusting KCard slots content. In the following example, some metadata pills are hidden on smaller screens:

- + { + if (!newValue) { + return; + } const { cardsPerRow, columnGap, rowGap } = newValue; gridStyle.value = { @@ -62,6 +65,16 @@ }; }, props: { + /** + * The id of the grid unique to a page + */ + // used from 'useGridConfig' + /* eslint-disable-next-line kolibri/vue-no-unused-properties */ + gridId: { + type: String, + required: true, + }, + /* eslint-enable-next-line kolibri/vue-no-unused-properties */ /** * Sets the base grid layout. * @@ -78,7 +91,7 @@ */ // eslint-disable-next-line kolibri/vue-no-unused-properties layout: { - required: false, + required: true, type: String, default: '1-2-2', validator: value => { diff --git a/lib/cards/gridBaseLayouts.js b/lib/cards/gridBaseLayouts.js index 35b6fdb51..a8ef7f586 100644 --- a/lib/cards/gridBaseLayouts.js +++ b/lib/cards/gridBaseLayouts.js @@ -3,19 +3,8 @@ * corresponding to the most commonly used grids in our designs. */ -// Breakpoint levels -// Correspond to https://design-system.learningequality.org/layout/#responsiveness -const LEVEL_0 = 'level-0'; -const LEVEL_1 = 'level-1'; -const LEVEL_2 = 'level-2'; -const LEVEL_3 = 'level-3'; -const LEVEL_4 = 'level-4'; -const LEVEL_5 = 'level-5'; -const LEVEL_6 = 'level-6'; -const LEVEL_7 = 'level-7'; - // Settings common to all breakpoint levels -const levelCommon = { +const breakpointCommon = { columnGap: '30px', rowGap: '30px', }; @@ -24,39 +13,42 @@ const levelCommon = { * Configuration for '1-1-1' grid, * that is a grid with 1 card per row * on all screen sizes. + * + * Organized by breakpoint levels as defined in + * https://design-system.learningequality.org/layout/#responsiveness */ const LAYOUT_CONFIG_1_1_1 = { - [LEVEL_0]: { + 0: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_1]: { + 1: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_2]: { + 2: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_3]: { + 3: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_4]: { + 4: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_5]: { + 5: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_6]: { + 6: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_7]: { + 7: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, }; @@ -66,39 +58,42 @@ const LAYOUT_CONFIG_1_1_1 = { * - 1 card per row on smaller screens * - 2 cards per row on medium screens * - 2 cards per row on larger screens + * + * Organized by breakpoint levels as defined in + * https://design-system.learningequality.org/layout/#responsiveness */ const LAYOUT_CONFIG_1_2_2 = { - [LEVEL_0]: { + 0: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_1]: { + 1: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_2]: { + 2: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_3]: { + 3: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_4]: { + 4: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_5]: { + 5: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_6]: { + 6: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_7]: { + 7: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, }; @@ -108,55 +103,49 @@ const LAYOUT_CONFIG_1_2_2 = { * - 1 card per row on smaller screens * - 2 cards per row on medium screens * - 3 cards per row on larger screens + * + * Organized by breakpoint levels as defined in + * https://design-system.learningequality.org/layout/#responsiveness */ const LAYOUT_CONFIG_1_2_3 = { - [LEVEL_0]: { + 0: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_1]: { + 1: { cardsPerRow: 1, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_2]: { + 2: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_3]: { + 3: { cardsPerRow: 2, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_4]: { + 4: { cardsPerRow: 3, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_5]: { + 5: { cardsPerRow: 3, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_6]: { + 6: { cardsPerRow: 3, - ...levelCommon, + ...breakpointCommon, }, - [LEVEL_7]: { + 7: { cardsPerRow: 3, - ...levelCommon, + ...breakpointCommon, }, }; export const LAYOUT_1_1_1 = '1-1-1'; export const LAYOUT_1_2_2 = '1-2-2'; export const LAYOUT_1_2_3 = '1-2-3'; -export const LEVELS = { - 0: LEVEL_0, - 1: LEVEL_1, - 2: LEVEL_2, - 3: LEVEL_3, - 4: LEVEL_4, - 5: LEVEL_5, - 6: LEVEL_6, - 7: LEVEL_7, -}; + export const LAYOUT_CONFIGS = { [LAYOUT_1_1_1]: LAYOUT_CONFIG_1_1_1, [LAYOUT_1_2_2]: LAYOUT_CONFIG_1_2_2, diff --git a/lib/cards/useGridConfig.js b/lib/cards/useGridConfig.js new file mode 100644 index 000000000..c49591e62 --- /dev/null +++ b/lib/cards/useGridConfig.js @@ -0,0 +1,72 @@ +import '../composables/composition-api'; // TODO: Remove as soon as not needed +import cloneDeep from 'lodash/cloneDeep'; +import { reactive, ref, watch, onBeforeUnmount } from '@vue/composition-api'; + +import useKResponsiveWindow from '../composables/useKResponsiveWindow'; + +import { LAYOUT_CONFIGS } from './gridBaseLayouts'; + +/** + * Stores grids configurations + * { : { gridId: ..., layoutConfig: { ... } }} + * where layoutConfig has format as defined in LAYOUT_CONFIGS + */ +const gridConfigs = reactive({}); + +/** + * Contains logic related to 'KCardGrid' configuration + * - When 'KCardGrid' instance is initialized, stores its + * base layout configuration to the global `gridConfigs` + * so that it can be later customized via `useKCardGrid` + * (this is one part of a robust approach that accounts + * for `useKCardGrid` being called on a page that has more + * than one `KCardGrid`) + * - Observes window size and returns the final grid configuration + * to 'KCardGrid' for the current breakpoint + */ +export default function useGridConfig(props) { + const currentBreakpointConfig = ref({}); + const { windowBreakpoint } = useKResponsiveWindow(); + + const gridId = props.gridId; // TODO: Document not reactive + const layout = props.layout; // TODO: Document not reactive + + // retrieve definition and save the whole base layout + // configuration object for a chosen KCardGrid's layout + // ('cloneDeep' to secure LAYOUT_CONFIGS from overrides) + const baseLayoutConfig = cloneDeep(LAYOUT_CONFIGS[layout]); + if (!Object.keys(gridConfigs).includes(gridId)) { + gridConfigs[gridId] = { gridId, layoutConfig: {} }; + } + gridConfigs[gridId].layoutConfig = baseLayoutConfig; + + /** + * Returns part of the grid configuration for the given breakpoint + */ + function getBreakpointConfig(gridId, breakpoint) { + const layoutConfig = gridConfigs[gridId].layoutConfig; + const breakpointConfig = layoutConfig[breakpoint]; + return breakpointConfig; + } + + watch( + windowBreakpoint, + (newBreakpoint, oldBreakpoint) => { + if (newBreakpoint === null || newBreakpoint === undefined) { + currentBreakpointConfig.value = getBreakpointConfig(gridId, 0); + } + if (newBreakpoint !== oldBreakpoint) { + currentBreakpointConfig.value = getBreakpointConfig(gridId, newBreakpoint); + } + }, + { immediate: true } + ); + + onBeforeUnmount(() => { + delete gridConfigs[gridId]; + }); + + return { + currentBreakpointConfig, + }; +} diff --git a/lib/cards/useResponsiveGridLayout.js b/lib/cards/useResponsiveGridLayout.js deleted file mode 100644 index 0fecebc1b..000000000 --- a/lib/cards/useResponsiveGridLayout.js +++ /dev/null @@ -1,47 +0,0 @@ -import { watch, ref } from '@vue/composition-api'; - -import useKResponsiveWindow from '../composables/useKResponsiveWindow'; - -import { LAYOUT_CONFIGS, LEVELS } from './gridBaseLayouts'; - -/** - * Observes the window breakpoint level - * and returns the grid layout configuration - * object for the current breakpoint level. - */ -export default function useResponsiveGridLayout(props) { - const currentLevelConfig = ref({}); - - const { windowBreakpoint } = useKResponsiveWindow(); - - /** - * - * @param {Object} props `KCardGrid` props - * @param {Number} breakpoint The breakpoint level 0-7 - * - * @returns {Object} The grid layout configuration object - * for the given breakpoint level - */ - function getLevelLayoutConfig(props, breakpoint) { - const baseLayoutConfig = LAYOUT_CONFIGS[props.layout]; - const baseLevelConfig = baseLayoutConfig[LEVELS[breakpoint]]; - - return { ...baseLevelConfig }; - } - - watch( - windowBreakpoint, - (newBreakpoint, oldBreakpoint) => { - // can happen very briefly before the breakpoint value gets calculated - if (newBreakpoint === null) { - currentLevelConfig.value = getLevelLayoutConfig(props, 0); - } - if (newBreakpoint !== oldBreakpoint) { - currentLevelConfig.value = getLevelLayoutConfig(props, newBreakpoint); - } - }, - { immediate: true } - ); - - return { currentLevelConfig }; -}